o
    ' ˜j¢ƒ  ã                   @   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	m
Z
mZ ddlmZ ddlZddlZddlZddlZedƒZG d	d
„ d
ƒZdS )aÅ	  
position_manager.py

Owns the full lifecycle of a single position from entry to exit, and
the collection of all currently open positions. Talks to
alpaca_client.py for order execution (skipped entirely in simulation
mode) and risk_manager.py for stop math.

State shape per position (matches project's required fields):
{
  "symbol", "entry_price", "entry_time", "initial_stop", "current_stop",
  "highest_price", "current_price", "trailing_distance", "shares",
  "current_pl", "mfe" (max favorable excursion), "exit_price",
  "exit_reason", "status",  # "open" | "closed" | "closing"
  "fade_grace_active", "grace_extensions_used"  # [FEATURE 2026-08-18]
  "exit_pending", "exit_order_id", "exit_reason_pending"  # [BUGFIX 2026-08-18]
}

[BUGFIX 2026-08-18] Order-race / "wash trade" fix
---------------------------------------------------
Previously, exit_position() called client.close_position(symbol) and, on
ANY exception (including Alpaca's "potential wash trade detected...
opposite side market/stop order exists" rejection), simply logged an
ERROR and returned without changing local state. Because status stayed
"open", the next 5-second poll cycle (monitor.py's poll_interval_seconds_
intraday) saw the position as still open, re-evaluated the trailing
stop, and called exit_position() again -- submitting ANOTHER close order
against a broker that already had the first one resting and unfilled.
Alpaca's wash-trade guard then rejected the new one too, and the cycle
repeated every poll until the original order finally filled (seen live
as 3+ minutes of repeated rejections on a single symbol, during which
the position had no working stop protection at all).

The fix makes exit_position() idempotent per symbol:
  - status gains a third value, "closing", set the moment we have a
    close order (submitted by us, or a pre-existing one we recovered
    from a wash-trade rejection) resting at the broker.
  - While "closing", exit_position() never submits a new order -- it
    polls the existing order via alpaca_client.get_order() and only
    finalizes the position once that order is actually FILLED, using
    the broker-confirmed fill price (not the price bars happened to
    show when the exit signal first fired).
  - If close_position() itself raises a wash-trade rejection, we parse
    the existing_order_id Alpaca already hands us in that error (it was
    previously discarded) and adopt it as the order we're tracking,
    instead of treating the rejection as a failure.
é    )ÚdatetimeÚ	timedeltaÚtimezone)Ú
get_config)Ú
get_logger)Ú
get_clientÚparse_wash_trade_errorÚparse_position_not_found_error)Ú	OrderSideNÚposition_managerc                   @   sž  e Zd Zd?defdd„Zdd„ Zedefdd	„ƒZd
e	f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dededed
efdd„ZdAdededefdd „Zdedededed
ef
d!d"„Zded#eded
efd$d%„Zded&ed
efd'd(„Zdeded)efd*d+„Zd&efd,d-„Zded
efd.d/„Zded#eded
efd0d1„ZdBd3d4„Zd5d6„ Zd
efd7d8„Zd
efd9d:„Zd;d<„ Z d=d>„ Z!dS )CÚPositionManagerTÚ
simulationc                 C   s2   || _ |stƒ nd | _t ¡ | _|  ¡  d | _d S ©N)r   r   ÚclientÚ
data_storeÚload_positionsÚ	positionsÚ_prune_stale_closed_positionsÚ_last_reconcile)Úselfr   © r   úL/var/www/screener/trade/premarket_backup_2026-09-08_2010/position_manager.pyÚ__init__@   s
   

zPositionManager.__init__c                    sj   t  ¡  ¡ ‰‡ ‡fdd„ˆ j ¡ D ƒ}|D ]}ˆ j|= q|r3t dt|ƒ› d|› �¡ t 	ˆ j¡ dS dS )a†  
        [BUGFIX 2026-08-31] positions.json is never rotated -- it's loaded
        wholesale from disk with no date filtering, so "closed" positions
        from any prior day pile up indefinitely (confirmed live: entries
        going back to 2026-08-17 still present). Because
        has_closed_position_today() and reentry_cooldown_remaining_minutes()
        both just check status == "closed" on this same self.positions dict
        with no date check of their own, a symbol that closed once, ever,
        looks identical to one that closed minutes ago -- permanently
        forcing the score-based re-entry gate's full-confirmation path (and,
        if same_symbol_reentry_cooldown_minutes is ever re-enabled, an
        indefinite phantom cooldown). Same underlying gap already found in
        state/intraday_health.json (see monitor.py's health-state pruning).

        Only "closed" entries are pruned -- "open"/"closing" positions are
        always kept regardless of date, since one from a prior day is most
        likely a real position recovered after a crash/restart that still
        needs active management, not stale data.
        c                    s0   g | ]\}}|  d ¡dkrˆ  |¡ˆkr|‘qS )ÚstatusÚclosed)ÚgetÚ_exit_et_date)Ú.0ÚsymbolÚp©r   Útodayr   r   Ú
<listcomp>\   s    ÿzAPositionManager._prune_stale_closed_positions.<locals>.<listcomp>z[STARTUP] Pruned z& closed position(s) from a prior day: N)
Úmarket_timeÚnow_etÚdater   ÚitemsÚlogÚinfoÚlenr   Úsave_positions)r   Ústaler   r   r    r   r   G   s   
þz-PositionManager._prune_stale_closed_positionsr   c                 C   s"   |   d¡}|s	d S t t |¡¡S )NÚ	exit_time)r   r#   Úet_dater   Úfromisoformat)r   r,   r   r   r   r   d   s   
zPositionManager._exit_et_dateÚreturnc                 C   s   t dd„ | j ¡ D ƒƒS )Nc                 S   s   g | ]
}|d  dv r|‘qS )r   ©ÚopenÚclosingr   ©r   r   r   r   r   r"   o   s    z8PositionManager.open_positions_count.<locals>.<listcomp>)r)   r   Úvalues©r   r   r   r   Úopen_positions_countl   s   z$PositionManager.open_positions_countc                 C   s   t ƒ d d }|  ¡ |k S )NÚtradingÚmax_positions)r   r6   )r   r8   r   r   r   Úhas_available_slotq   s   z"PositionManager.has_available_slotr   c                 C   s    | j  |¡}|d uo|d dv S )Nr   r0   ©r   r   ©r   r   r   r   r   r   Úis_symbol_openu   s   zPositionManager.is_symbol_openc                 C   s    | j  |¡}|duo|d dkS )a0  
        [FEATURE 2026-08-27] True if this symbol has a closed position
        already on record this session. Feeds entry_engine.evaluate_entry()'s
        is_reentry parameter -- the score-based re-entry gate (see
        entry.require_full_confirmation_on_reentry in config.json)
        that replaced the old same_symbol_reentry_cooldown_minutes
        approach below. Same underlying self.positions lookup that
        method already used, just answering "has this symbol traded
        today at all" instead of "how long since it last exited."
        Nr   r   r:   r;   r   r   r   Úhas_closed_position_todayy   s   z)PositionManager.has_closed_position_todayc                 C   sz   | j  |¡}|du s|d dks| d¡sdS tƒ d  dd¡}|dkr&dS t |d ¡}t tj¡|  ¡ d }|| S )	a¬  
        [FEATURE 2026-08-25, superseded 2026-08-27] Minutes remaining in
        the same-symbol re-entry cooldown, or None if the symbol has no
        prior closed position on record (never traded, or the record
        was already overwritten by a newer entry -- in which case that
        newer entry's own timing governs, not this one). 0 or negative
        means the cooldown has elapsed.

        Retained for anyone who wants to combine a time-based wait WITH
        the newer score-based gate (has_closed_position_today() above),
        but same_symbol_reentry_cooldown_minutes defaults to 0
        (disabled) as of 2026-08-27 -- the score-based gate is now the
        primary re-entry control. Exposed separately from
        enter_position()'s hard check so monitor.py's shortlist can
        also filter on this before spending a full evaluate_entry()
        call on a symbol that would just get rejected anyway.
        Nr   r   r,   r7   Ú$same_symbol_reentry_cooldown_minutesr   g      N@)	r   r   r   r   r.   Únowr   ÚutcÚtotal_seconds)r   r   r   Úcooldown_minutesr,   Úelapsed_minutesr   r   r   Ú"reentry_cooldown_remaining_minutes‡   s   z2PositionManager.reentry_cooldown_remaining_minutesNÚ Úentry_priceÚaccount_equityÚbarsÚreasonc                 C   s8  t ƒ d d }|  |¡r|st d|› d�¡ d S t ƒ d  dd¡}|dkrE|  |¡}|d urE|dkrEt d|› d|d›d	|› d
�¡ d S |  ¡ sTt d|› d�¡ d S t 	||¡}	t 
|||	¡}
|
dkrvt d|› d|› d|	› d�¡ d S d }| jsªz| j ||
d¡}t|dd ƒ}W n ty© } zt d|› d|› �¡ W Y d }~d S d }~ww i d|“d|“dt tj¡ ¡ “d|	“d|	“d|“d|“dt||	 dƒ“d|
“dd“dd“dd “d d “d!d"“d#|“d$|“d%d“dd&d d d d'œ¥}|| j|< |  ¡  t d(|› d)|d*›d+|
› d,|› d-�	¡ t d.|› d/|	d*›�¡ |S )0Nr7   Ú"allow_multiple_entries_same_symbolz[ENTRY] Skipped z: already have an open positionr>   r   ú: z.1fzmin remaining in z!min same-symbol re-entry cooldownz: no available position slotsz#: computed size is 0 shares (entry=z, stop=ú)ÚbuyÚidz$[ENTRY] Order submission failed for r   rF   Ú
entry_timeÚinitial_stopÚcurrent_stopÚhighest_priceÚcurrent_priceÚtrailing_distanceé   ÚsharesÚ
current_plç        ÚmfeÚ
exit_priceÚexit_reasonr   r1   Úorder_idÚentry_reasonÚfade_grace_activeF)Úgrace_extensions_usedÚexit_pendingÚexit_order_idÚexit_reason_pendingÚexit_retry_afterz[ENTRY] ú @ $ú.2fz shares=z	 reason='ú'z[STOP] z initial stop=$)r   r<   r'   Úwarningr   rD   r(   r9   Úrisk_managerÚcompute_initial_stopÚcompute_position_sizer   r   Úsubmit_market_orderÚgetattrÚ	ExceptionÚerrorr   r?   r   r@   Ú	isoformatÚroundr   Ú_save)r   r   rF   rG   rH   rI   Úallow_multirB   Ú	remainingrP   rV   r\   ÚorderÚeÚpositionr   r   r   Úenter_position¤   s    

ÿÿ
ÿ€þÿþýüûúùø	÷
öõôóòñðíã
&zPositionManager.enter_positionrS   c              	   C   sø   | j  |¡}|du s|d dkrdS ||d< ||d kr ||d< t |d |d |¡}||d krG||d< t d|› d|d d	›d
|d	›�¡ t||d  |d  dƒ|d< t|d |d  |d  dƒ}t|d |ƒ|d< |  ||||¡}|  	¡  |S )ah  
        Called every poll cycle for each open position. Updates
        highest_price / current_stop per the strict "only move up"
        trailing rule (risk_manager.py -- UNCHANGED by the logic below),
        and returns an exit signal if the stop was hit and the exit
        wasn't forgiven by fade-confirmation (see _check_fade_confirmed_exit).
        Nr   r1   rS   rR   rQ   ú[TRAIL] z high=$re   z stop=$rF   rV   é   rW   rY   )
r   r   rh   Úupdate_trailing_stopr'   r(   rp   ÚmaxÚ_check_fade_confirmed_exitrq   )r   r   rS   rH   r   Únew_stoprY   Úexit_signalr   r   r   Úupdate_positioný   s    $zPositionManager.update_positionc                 C   sê  t ƒ d  di ¡}|d }||kr0| dd¡dkr&t d|› d|d	›d
�¡ d|d< d|d< dS || dd¡ }||kr>dS | dd¡}| dd¡}	| dd¡s_t d|› d|d	›d�¡ dS |spt d|› d|d	›d�¡ dS |	|kr‰t d|› d|d	›d|	› d|› d�	¡ dS t ƒ d d }
t |||
¡}t |¡r³t d|› d|d	›d|j› d|j› d�	¡ dS | dd¡}| dd¡| |d< |	d |d< ||d  }t d|› d|d	›d|j› d|j› d|d	›d |d › d|› d!|d	›�¡ dS )"a  
        [FEATURE 2026-08-18]

        The real trailing stop (p["current_stop"]) is computed entirely
        by risk_manager.py above, completely untouched by anything in
        this method -- it still only ever moves up, exactly as before
        and as covered by risk_manager's own tests.

        What changes: touching that stop no longer exits immediately by
        default. Instead:
          1. If price is still above the real stop -> nothing to do;
             reset any active grace (a genuinely new dip should always
             get a fresh grace budget, not one partially used by an
             earlier, unrelated pullback).
          2. If price is below the real stop but still within a
             previously-granted grace window -> hold, no new checks.
          3. If price has breached even the grace-widened floor (or
             this is the first touch, no grace active yet):
             - Not confirmed fading (intraday_health says this doesn't
               look like real deterioration) AND grace budget remains
               -> widen the effective floor by
               stop.fade_confirmation.grace_distance and hold.
             - Confirmed fading, OR grace budget exhausted, OR
               fade-confirmation is disabled, OR no bar data available
               to evaluate health -> exit ("fail toward exiting", not
               toward holding, when we can't confirm one way or the
               other -- this protects capital over giving every dip
               the benefit of the doubt indefinitely).
        ÚstopÚfade_confirmationrQ   r^   rX   r   rx   z recovered above stop $re   z; fade-confirmation grace resetr_   NÚmax_grace_extensionsé   ÚenabledFz stop touched @ $z& (fade-confirmation disabled); exitingÚTRAILING_STOPz' (no bar data to confirm fade); exitingz (grace extensions exhausted ú/z
); exitingÚuniverseÚmin_avg_daily_volumez( -- health confirms fading (price_slope=z, vwap_slope=Úgrace_distancegš™™™™™©?z. but health not confirmed fading (price_slope=z) -- granting $z grace (z), effective floor now $)	r   r   r'   r(   Úintraday_healthÚcompute_healthÚis_confirmed_fadingÚprice_slope_classÚvwap_slope_class)r   r   r   rS   rH   ÚcfgrQ   Úeffective_floorÚ	max_graceÚ
grace_usedÚavg_vol_baselineÚreadingr‰   Ú	new_floorr   r   r   r|     sj   ÿ
ÿ
ÿ
þÿþþýýýz*PositionManager._check_fade_confirmed_exitrZ   c                 C   sè  | j  |¡}|du s|d dvrdS | jr|  |||¡S |d dkr'|  |¡S | d¡}|durHt tj¡}t	|tƒr=|nt 
|¡}||k rHdS z| j |¡}t|ddƒ}	W nt tyÊ }
 zht|
ƒ}|dur{|  ||¡r{t d|› d|› d	�¡ |}	nE|durœt d|› d
|› d|› d�¡ |  |||
¡ W Y d}
~
dS t|
ƒr®|  |||
¡ W Y d}
~
dS t d|› d|
› �¡ W Y d}
~
dS W Y d}
~
nd}
~
ww d|d< d|d< |	|d< ||d< d|d< |  ¡  t d|› d|	› d�¡ |  |¡S )aŸ  
        Idempotent per symbol. [BUGFIX 2026-08-18, extended 2026-08-24]

        Safe to call every poll cycle for a symbol that's already exiting
        -- it will never submit a second close order while one is
        in flight. First call for an "open" position submits the close
        (or adopts an already-resting one if Alpaca rejects it as a wash
        trade) and moves status to "closing". Every subsequent call for a
        "closing" position just polls that order and finalizes once it's
        actually filled.

        [BUGFIX 2026-08-24] A second, distinct rejection -- Alpaca's
        "position not found" (code 40410000, NOT the wash-trade code the
        2026-08-18 fix covers) -- was still causing the exact same
        blind-retry-every-cycle symptom the 08-18 fix was written to
        eliminate. Real logs from that day (CRML/FSM/EXK/XPON) show 4-8
        consecutive "position not found" rejections, 20-30 seconds apart,
        over 4-6 minutes, with the position completely unprotected the
        whole time. Root cause: this error fires when the ENTRY order
        itself hasn't settled at the broker yet, even though local state
        already marked the position "open" at submission time (market
        buys can occasionally take longer to settle than one 5-second
        poll interval, especially during the order-flow crunch right at
        market open). See exit_retry_after below.
        Nr   r0   r2   rc   rN   ú[EXIT] z2: close order already resting at broker (order_id=z#); adopting instead of resubmittingz#: wash-trade rejection named order z( but it is not a resting SELL order for z€ (most likely the still-unsettled entry order) -- refusing to adopt it as the exit to avoid a phantom close; backing off insteadú+[EXIT] Failed to close broker position for rK   Tr`   ra   rb   z": close order submitted (order_id=z), awaiting fill)r   r   r   Ú_finalize_exitÚ_poll_pending_exitr   r?   r   r@   Ú
isinstancer.   r   Úclose_positionrl   rm   r   Ú_is_resting_sell_orderr'   rg   Ú_handle_position_not_foundr	   rn   rq   r(   )r   r   rZ   rI   r   Úretry_afterr?   Úretry_after_dtrt   r\   ru   Úexisting_order_idr   r   r   Úexit_positionq  sb   

ÿ
ÿÿ
þÚ€ö2
zPositionManager.exit_positionr\   c                 C   s:   | j  |¡}|du rdS t|ddƒ|kot|ddƒtjkS )a   
        [BUGFIX 2026-09-02] Verifies an order_id surfaced by a wash-trade
        rejection is actually a resting SELL order for this symbol before
        exit_position() trusts it as "our close order" -- see the long
        comment at its call site for the phantom-exit failure this
        prevents. Fails closed (returns False) on any lookup problem, so
        an unverifiable order is never adopted.
        NFr   Úside)r   Ú	get_orderrl   r
   ÚSELL)r   r   r\   rt   r   r   r   rœ   ß  s   	ÿz&PositionManager._is_resting_sell_orderÚexcc              	   C   s¨   t ƒ  di ¡ di ¡}| dd¡}| d¡}|r|  |¡nd}|du rGt tj¡t|d�  ¡ |d	< |  	¡  t
 d
|› d|› d|› d�¡ dS t
 d|› d|› �¡ dS )a‘  
        [BUGFIX 2026-08-24] Distinguishes the benign, expected race
        (entry order hasn't settled yet -- log at INFO, back off, don't
        hammer retries) from a genuinely unusual case (entry settled but
        broker still has no position -- log at ERROR same as before,
        since we don't have evidence for what that would mean or how to
        safely auto-recover from it).
        r€   Ú
exit_retryÚ"position_not_found_backoff_secondsé   r\   NF)Úsecondsrc   r–   z:: close rejected as 'position not found', but entry order zC hasn't settled at the broker yet -- this is expected, backing off z%s instead of resubmitting immediatelyr—   rK   )r   r   Ú_entry_order_settledr   r?   r   r@   r   ro   rq   r'   r(   rn   )r   r   r   r¥   r�   Úbackoff_secondsÚentry_order_idÚentry_settledr   r   r   r�   î  s    	
þÿ
þz*PositionManager._handle_position_not_foundc                 C   sD   | j  |¡}|du rdS tt|ddƒƒ ¡ }d|v r d|vr dS dS )zµReturns True if the given order is FILLED, False if it's
        still open/pending, None if the lookup itself failed (treated
        as 'unknown' by the caller, not as 'settled').Nr   rE   ÚfilledÚ	partiallyTF)r   r£   Ústrrl   Úlower)r   r\   rt   r   r   r   r   rª     s   z$PositionManager._entry_order_settledc              	   C   s  |d }|  d¡}|s"t d|› d�¡ d|d< d|d< |  ¡  d S | j |¡}|d u r.d S tt|dd	ƒƒ ¡ }d
|v r]d|vr]t|dd ƒ}|d urNt	|ƒn|d }|  
|||  dd¡¡S |dv r‚t d|› d|› d|› d�¡ d|d< d|d< d |d< |  ¡  d S d S )Nr   ra   r–   z_ is 'closing' with no tracked order_id; resetting to 'open' so the next cycle resubmits a closer1   r   Fr`   rE   r®   r¯   Úfilled_avg_pricerS   rb   r…   )ÚcanceledÚexpiredÚrejectedz: tracked close order z ended in status=z* without filling; will resubmit next cycle)r   r'   rn   rq   r   r£   r°   rl   r±   Úfloatr˜   rg   )r   r   r   r\   rt   r   Ú
fill_pricerZ   r   r   r   r™     s4   

ÿz"PositionManager._poll_pending_exitc                 C   sÈ   |d }||d< ||d< t  tj¡ ¡ |d< d|d< d|d< t||d	  |d
  dƒ|d< |  ¡  t |¡ t	 
d|› d|d›�¡ t	 
d|› �¡ |d dkrRdnd}t	 
d|› d|d d›�¡ |S )Nr   rZ   r[   r,   r   r   Fr`   rF   rV   ry   rW   r–   rd   re   z[EXIT_REASON] r   ú+rE   z[P/L] ú$)r   r?   r   r@   ro   rp   rq   r   Úappend_trade_recordr'   r(   )r   r   rZ   rI   r   Úpl_signr   r   r   r˜   E  s   
zPositionManager._finalize_exitÚ
END_OF_DAYc                 C   s:   t | j ¡ ƒD ]\}}|d dv r|  ||d |¡ qd S )Nr   r0   rS   )Úlistr   r&   r¡   )r   rI   r   r   r   r   r   Úliquidate_allX  s
   €þzPositionManager.liquidate_allc              	   C   s8   |   ¡ D ]}|  || j| d | j|  dd¡¡ qdS )zÀCall once per poll cycle before anything else touches open
        positions -- advances any "closing" position toward finalization
        without re-running trailing-stop/health logic on it.rS   rb   r…   N)Úget_closing_symbolsr¡   r   r   )r   r   r   r   r   Úpoll_pending_exits]  s
   ÿÿz"PositionManager.poll_pending_exitsc                 C   ó   dd„ | j  ¡ D ƒS )a  Symbols still eligible for trailing-stop/health evaluation.
        Excludes "closing" positions on purpose -- once an exit is in
        flight there's nothing to trail, and re-running update_position()
        on it would just be wasted work (the position is leaving).c                 S   ó    g | ]\}}|d  dkr|‘qS )r   r1   r   ©r   Úsr   r   r   r   r"   j  ó     z4PositionManager.get_open_symbols.<locals>.<listcomp>©r   r&   r5   r   r   r   Úget_open_symbolse  s   z PositionManager.get_open_symbolsc                 C   rÁ   )Nc                 S   rÂ   )r   r2   r   rÃ   r   r   r   r"   m  rÅ   z7PositionManager.get_closing_symbols.<locals>.<listcomp>rÆ   r5   r   r   r   r¿   l  s   z#PositionManager.get_closing_symbolsc                 C   s   t  | j¡ d S r   )r   r*   r   r5   r   r   r   rq   o  s   zPositionManager._savec           	   
   C   s  | j rdS tƒ d  dd¡}t tj¡}| jdur$|| j  ¡ |k r$dS || _zdd„ | j	 
¡ D ƒ}W n tyN } zt d|› �¡ W Y d}~dS d}~ww t|  ¡ ƒ}t| ¡ ƒ}|| }|| }|rpt dt|ƒ› d	�¡ |rt d
t|ƒ› d	�¡ dS dS )u/  
        Cross-checks local position state against Alpaca's actual open
        positions. Logs (but does not silently auto-liquidate) any
        mismatch, since prior systems have shown that automatically
        adopting/liquidating unexplained broker-side quantities can
        itself cause losses â€” a human should review true orphans.

        [BUGFIX 2026-09-02] Throttled to schedule.reconcile_interval_seconds
        (default 30s), matching the pattern _update_intraday_health() and
        _run_intraday_full_rescan() already use. Previously this fired an
        Alpaca API call on every single 5s main-loop cycle with no gate at
        all, which is the documented cause of 2026-09-01's 84 RECONCILE
        warnings and 2026-09-02's 4,514 (42% of that day's entire log
        volume) -- almost all of them the same expected one-cycle
        settlement lag right after an entry, not real orphans. That noise
        is exactly what buried the one day a real orphan (OWL/NVAX, see
        _is_resting_sell_order()) sat unreconciled for 6+ hours.
        NÚscheduleÚreconcile_interval_secondsé   c                 S   s   i | ]}|j |“qS r   )r   r3   r   r   r   Ú
<dictcomp>‘  s    z9PositionManager.reconcile_with_broker.<locals>.<dictcomp>z.[RECONCILE] Failed to fetch broker positions: z6[RECONCILE] Broker has positions not tracked locally: z. Review manually.zH[RECONCILE] Local ledger shows open positions the broker does not have: )r   r   r   r   r?   r   r@   r   rA   r   Úget_open_positionsrm   r'   rn   ÚsetrÇ   Úkeysrg   Úsorted)	r   Úintervalr?   Úbroker_positionsru   Ú
local_openÚbroker_openÚmissing_locallyÚmissing_at_brokerr   r   r   Úreconcile_with_brokers  s8   
€þ
ÿÿÿz%PositionManager.reconcile_with_broker)T)NrE   r   )r¼   )"Ú__name__Ú
__module__Ú__qualname__Úboolr   r   ÚstaticmethodÚdictr   Úintr6   r9   r°   r<   r=   rD   r¶   r½   rw   r   r|   r¡   rœ   rm   r�   rª   r™   r˜   r¾   rÀ   rÇ   r¿   rq   rÖ   r   r   r   r   r   ?   sH    ÿÿÿ
ÿYÿ
ÿVn!*
r   )Ú__doc__r   r   r   Úconfig_loaderr   Úlogger_setupr   Úalpaca_clientr   r   r	   Úalpaca.trading.enumsr
   rh   rŠ   r   r#   r'   r   r   r   r   r   Ú<module>   s    0