o
    /ø²j+$  ã                   @   sj   d Z ddlZddl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ZedƒZG dd	„ d	ƒZdS )
aà  
position_manager.py

Owns local position state for this project: sizing a new position off
smart_engine.py's own Stage 3 stop (no separate risk_manager here --
that stop is already computed at entry-decision time and reused
directly), submitting entry/exit orders through alpaca_client, and
persisting to data_store so a mid-day restart doesn't lose track of
what's actually open.

Deliberately leaner than screener/premarket's position_manager.py: no
momentum-fade exit logic (that's exit.py's job, called from
monitor.py), no entry-attempt tracker, no fade-confirmed exit. Reuses
the same wash-trade / position-not-found error handling already coded
into alpaca_client.py, since those are real broker-timing races
already proven to happen against this same paper-account family, not
something worth reinventing.

Known simplification: a filled position is recorded locally at the
smart_engine decision price (the market price at the instant BUY
fired), not the order's actual average fill price -- a market order
can fill a few cents away. Same simplification alpaca_client.py's own
docstrings note sip_bot's position_manager.py makes at entry time.
Acceptable for now; revisit if fill slippage turns out to matter once
there's real trade data.
é    N)ÚdatetimeÚtimezone)Ú
get_config)Ú
get_logger)Ú
get_clientÚparse_wash_trade_errorÚparse_position_not_found_errorÚposition_managerc                   @   sÖ   e Zd Z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
fdd„Zdedededefdd„Z	d"de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dedefdd„Zd d!„ ZdS )#ÚPositionManagerc                 C   s"   t ƒ | _tƒ d | _t ¡ | _d S )NÚtrading)r   Úclientr   ÚcfgÚ
data_storeÚload_positionsÚ	positions©Úself© r   úposition_manager.pyÚ__init__(   s   zPositionManager.__init__Úreturnc                 C   s
   t | jƒS ©N)Úlenr   r   r   r   r   Úopen_positions_count.   ó   
z$PositionManager.open_positions_countc                 C   s   |   ¡ | jd k S )NÚmax_positions)r   r   r   r   r   r   Úhas_available_slot1   s   z"PositionManager.has_available_slotÚsymbolc                 C   s
   || j v S r   )r   )r   r   r   r   r   Úis_symbol_open4   r   zPositionManager.is_symbol_openc                 C   s   t | j ¡ ƒS r   )Úlistr   Úkeysr   r   r   r   Úget_open_symbols7   s   z PositionManager.get_open_symbolsÚequityÚentry_priceÚ
stop_pricec                 C   st   || }|dks
|sdS || j d  d }t || ¡}|| j d  d }t|t || ¡ƒ}|| j d kr8|S dS )z¹Risk account_risk_pct_per_trade% of equity on the distance
        between entry and the supplied stop, capped so no single
        position exceeds max_position_notional_pct_of_equity.r   Úaccount_risk_pct_per_tradeç      Y@Ú#max_position_notional_pct_of_equityÚ
min_shares)r   ÚmathÚfloorÚmin)r   r"   r#   r$   Úrisk_per_shareÚrisk_dollarsÚqtyÚnotional_capr   r   r   Úcalculate_qty;   s   zPositionManager.calculate_qtyNÚreasonsc                 C   s>  |   |¡s	|  ¡ sdS | j ¡ }t|jƒ}|  |||¡}|dkr7t d|› d|d›d|d›d|d›d�	¡ dS z
| j 	||d	¡}W n t
y_ }	 zt d|› d
|	› �¡ W Y d }	~	dS d }	~	ww t tj¡}
|||||
 ¡ tt|ddƒƒ|pwg dœ| j|< |  ¡  t d|› d|› d|d›d|d›dd |p—g ¡› d�¡ dS )NFr   z[ENTRY] z sized to 0 shares (equity=$ú.2fz	, entry=$z, stop=$z) -- skippedÚbuyz order submission failed: ÚidÚ )r   r.   r#   r$   Ú
entry_timeÚentry_order_idr1   z BUY z @ ~$z (stop $z, reasons: z; ú)T)r   r   r   Úget_accountÚfloatr"   r0   ÚlogÚinfoÚsubmit_market_orderÚ	ExceptionÚerrorr   Únowr   ÚutcÚ	isoformatÚstrÚgetattrr   Ú_saveÚjoin)r   r   r#   r$   r1   Úaccountr"   r.   ÚorderÚer@   r   r   r   Úenter_positionL   s@   

ÿ
ÿ€þüÿ
ÿzPositionManager.enter_positionÚ
exit_priceÚreasonc                 C   sÈ  | j  |¡}|s
d S z| j |¡ W nR tyd } zFt|ƒ}|r.|  ||¡s-W Y d }~d S n,t|ƒrH|  ||¡sAt	 
d|› d�¡ W Y d }~d S t	 d|› d|› �¡ W Y d }~d S W Y d }~nd }~ww | d¡rˆ| j |d ¡}|d urˆt|jp{dƒdkrˆtt|jƒƒ|d< |d r˜||d  |d  d nd	}||d  |d  }	i |¥|t tj¡ ¡ |d
t|dƒt|	dƒdœ¥}
t |
¡ | j |= |  ¡  t	 d|› d|d›d|› d|d›d|	d›d�¡ d S )Nú[EXIT] z[ broker reports no position yet (entry order likely still settling) -- will retry next pollz close_position failed: Úcancelled_entry_order_idr   r.   r#   r&   g        Úclosedé   é   )rK   Ú	exit_timeÚexit_reasonÚstatusÚpl_pctÚ
pl_dollarsz
 SELL @ ~$r2   ú (z	) -- P/L z+.2fz% ($r8   )r   Úgetr   Úclose_positionr>   r   Ú_resolve_wash_conflictr   Ú_entry_never_filledr;   Úwarningr?   Ú	get_orderr:   Ú
filled_qtyÚintr   r@   r   rA   rB   Úroundr   Úappend_trade_recordrE   r<   )r   r   rK   rL   ÚprI   Úorder_idrH   rU   rV   Útrader   r   r   Úexit_positionl   sR   ÿ÷€ø
$ÿý
ÿÿzPositionManager.exit_positionrc   c              	   C   sÒ   | j | }| j |¡}|du rt d|› d|› d�¡ dS t|jdt|jƒƒ ¡ }t|j	dt|j	ƒƒ ¡ }|dkrJt d|› d|› d	|› d
�¡ dS t d|› d|› d|› d�¡ | j 
|¡ ||d< |  ¡  dS )zËclose_position() was rejected because order_id is resting on
        the other side. True = the position is genuinely being closed
        (record the exit now); False = keep it tracked, retry next poll.NrM   z wash-trade conflict with zH but couldn't fetch that order -- keeping position, will retry next pollFÚvalueÚsellz	 a SELL (z, z2) is already working; treating as being closed outTz" close blocked by our resting BUY rW   z1); cancelling it and keeping the position trackedrN   )r   r   r]   r;   r\   rD   ÚsiderC   ÚlowerrT   Úcancel_orderrE   )r   r   rc   rb   rH   rh   rT   r   r   r   rZ   š   s   
z&PositionManager._resolve_wash_conflictrb   c              	   C   s´   |  d¡p	|  d¡}|r| j |¡nd}|du rdS t|jdt|jƒƒ ¡ }t|jp+dƒ}|dvs5|dkr7dS | j 	|¡dkrAdS t
 d|› d	|› d
|› d�¡ | j|= |  ¡  dS )zÅBroker says no position. If the entry order is dead with zero
        fill and the broker really holds nothing, stop tracking it (there
        was no trade); otherwise the entry is still settling.rN   r7   NFrf   r   )ÚcanceledÚexpiredÚrejectedrM   z entry order ú z5 with no fill -- no trade happened, dropping positionT)rX   r   r]   rD   rT   rC   ri   r:   r^   Úget_position_qtyr;   r\   r   rE   )r   r   rb   rc   rH   rT   Úfilledr   r   r   r[   ¶   s   z#PositionManager._entry_never_filledc                 C   s   t  | j¡ d S r   )r   Úsave_positionsr   r   r   r   r   rE   Ê   s   zPositionManager._saver   )Ú__name__Ú
__module__Ú__qualname__r   r_   r   Úboolr   rC   r   r   r!   r:   r0   rJ   re   rZ   Údictr[   rE   r   r   r   r   r
   '   s"    ÿÿ
ÿ .r
   )Ú__doc__r)   r   r   Úconfig_loaderr   Úlogger_setupr   Úalpaca_clientr   r   r   r   r;   r
   r   r   r   r   Ú<module>   s    