o
     ˜j%  ã                
   @   sÖ   d Z ddlZddlm  mZ ddlZddlZddl	Z	ddl
Z
ddlZddlmZmZmZ ej dej ej ej e¡¡¡¡ G dd„ dƒZG dd„ deƒZG dd	„ d	ƒZG d
d„ dejƒZedkrie ¡  dS dS )aQ  
test_wash_trade_exit_bugfix.py

[BUGFIX 2026-09-02] Regression coverage for two related fixes made after
reviewing 2026-09-02's live session logs:

1. exit_position()'s wash-trade handler used to adopt ANY existing_order_id
   Alpaca handed back in a wash-trade rejection as if it were a resting
   close (SELL) order, with no check that it actually was one. Live logs
   showed the rejection can instead name the position's own still-
   unsettled entry (BUY) order -- adopting that meant _poll_pending_exit()
   later saw the BUY fill and _finalize_exit() recorded it as the exit,
   marking the position "closed" locally (with a fabricated exit price)
   while the real shares stayed open, unmanaged, at the broker indefinitely
   (confirmed live for OWL and NVAX on 2026-09-02: RECONCILE flagged both
   as broker-side orphans from shortly after entry through market close).

2. reconcile_with_broker() previously had no interval throttle of its own
   and fired on every 5s main-loop cycle.

No real Alpaca calls are made anywhere in this file -- self.client is a
fake with hand-scripted responses.
é    N)ÚdatetimeÚ	timedeltaÚtimezonec                   @   s   e Zd Zddd„ZdS )Ú
_FakeOrderNc                 C   s"   || _ || _|| _|| _|| _d S ©N)ÚidÚsymbolÚsideÚstatusÚfilled_avg_price)Úselfr   r   r	   r
   r   © r   ú@/var/www/screener/premarket/tests/test_wash_trade_exit_bugfix.pyÚ__init__$   ó
   
z_FakeOrder.__init__r   )Ú__name__Ú
__module__Ú__qualname__r   r   r   r   r   r   #   s    r   c                       s   e Zd Z‡ fdd„Z‡  ZS )Ú_WashTradeErrorc                    s   t ƒ  d| ¡ d S )Nz¡{"code":40310000,"existing_order_id":"%s","message":"potential wash trade detected. use complex orders","reject_reason":"opposite side market/stop order exists"})Úsuperr   )r   Úexisting_order_id©Ú	__class__r   r   r   -   s
   ýÿz_WashTradeError.__init__)r   r   r   r   Ú__classcell__r   r   r   r   r   ,   s    r   c                   @   s0   e Zd ZdZdd„ Zdd„ Zdd„ Zdd	„ Zd
S )Ú_FakeClientz6Hand-scripted stand-in for alpaca_client.AlpacaClient.c                 C   s"   i | _ d| _d | _g | _d| _d S )Nr   )Úorders_by_idÚclose_position_callsÚclose_position_side_effectÚopen_positionsÚget_open_positions_calls©r   r   r   r   r   9   r   z_FakeClient.__init__c                 C   s&   |  j d7  _ | jd ur| j‚tdƒ‚)Né   z(test must set close_position_side_effect)r   r   ÚAssertionError)r   r   r   r   r   Úclose_position@   s   
z_FakeClient.close_positionc                 C   s   | j  |¡S r   )r   Úget)r   Úorder_idr   r   r   Ú	get_orderF   s   z_FakeClient.get_orderc                 C   s   |  j d7  _ | jS )Nr!   )r   r   r    r   r   r   Úget_open_positionsI   s   z_FakeClient.get_open_positionsN)r   r   r   Ú__doc__r   r#   r&   r'   r   r   r   r   r   6   s    r   c                   @   s4   e Zd Zdd„ Zdd„ Zdd„ Zdd„ Zd	d
„ ZdS )ÚWashTradeExitBugfixTestsc              	      s4  t  ¡ ˆ _t ¡ ˆ _t tj tj tj 	t
¡¡¡¡ dd l}|j ¡  dd ladd latjˆ _‡ fdd„}|t_t ¡  tjdd�ˆ _dˆ j_tƒ ˆ _ˆ jˆ j_i dd“d	d
“dt tj¡ ¡ “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%œ¥ˆ jjd< d S )&Nr   c                     sF   ddl m}  ˆ j}i }dD ]}| |ƒ| }|jddd� |||< q|S )Nr   )ÚPath)Ú	premarketÚintradayÚtradesÚstateT)ÚparentsÚexist_ok)Úpathlibr*   ÚtmpdirÚmkdir)r*   ÚbaseÚoutÚnameÚpr    r   r   Ú	fake_dirs]   s   
z1WashTradeExitBugfixTests.setUp.<locals>.fake_dirsT)Ú
simulationFr   ÚOWLÚentry_priceçÍÌÌÌÌL'@Ú
entry_timeÚinitial_stopg®Gáz'@Úcurrent_stopç®Gáz.'@Úhighest_pricegáz®Ga'@Úcurrent_priceÚtrailing_distanceg)\�Âõ(¼?Úsharesé˜   Ú
current_plç        ÚmfegìQ¸…ë@Ú
exit_priceÚexit_reasonr
   Úopenr%   úENTRY-OWL-1Úentry_reasonÚtestÚfade_grace_activer!   )Úgrace_extensions_usedÚexit_pendingÚexit_order_idÚexit_reason_pendingÚexit_retry_after)ÚtempfileÚmkdtempr2   ÚosÚgetcwdÚ	_orig_cwdÚchdirÚpathÚdirnameÚabspathÚ__file__Úconfig_loaderÚ
get_configÚcache_clearÚ
data_storeÚposition_managerÚ_dirsÚ
_orig_dirsÚensure_dirsÚPositionManagerÚpmr9   r   Úfake_clientÚclientr   Únowr   ÚutcÚ	isoformatÚ	positions)r   r_   r8   r   r    r   ÚsetUpO   sl   

"


ÿÿÿþþþýýýüüüüûûûúøzWashTradeExitBugfixTests.setUpc                 C   s(   t j| jdd� | jt_t | j¡ d S )NT)Úignore_errors)	ÚshutilÚrmtreer2   re   rb   rd   rW   rZ   rY   r    r   r   r   ÚtearDown~   s   z!WashTradeExitBugfixTests.tearDownc                 C   s”   ddl m} | jjd }tdƒ| j_tdd|jddd�| jj	d< | j 
ddd	¡}|  |¡ |  |d
 dd¡ |  |d ¡ |  | dd¡dd¡ dS )a5  The exact 2026-09-02 OWL/NVAX failure: the wash-trade rejection
        names the position's own still-open BUY order. That must NOT be
        adopted as the exit -- the position should stay 'open' (so the
        next cycle retries a clean close) rather than being marked
        'closed' off of a buy fill.r   ©Ú	OrderSider:   rL   Úfilledr<   ©r   r@   ÚTRAILING_STOPr
   rK   zCposition must NOT be marked closed/closing off an adopted BUY orderrR   rF   rG   gìQ¸…ëÀz0must not fabricate the historical bad exit priceN)Úalpaca.trading.enumsru   rh   rn   r   ri   r   r   ÚBUYr   Úexit_positionÚassertIsNoneÚassertEqualÚassertNotEqualr$   ©r   ru   r7   Úresultr   r   r   Ú5test_wash_trade_naming_own_entry_order_is_not_adopted„   s   ÿ
ÿÿzNWashTradeExitBugfixTests.test_wash_trade_naming_own_entry_order_is_not_adoptedc                 C   sŽ   ddl m} | jjd }tdƒ| j_tdd|jddd�| jj	d< | j 
ddd	¡}|  |¡ |  |d
 d¡ |  |d d¡ |  |d d¡ dS )zÂSanity check the fix doesn't break the legitimate case the
        2026-08-18 bugfix was written for: a genuine resting close/SELL
        order should still be adopted and polled to completion.r   rt   r:   zREAL-CLOSE-1rv   g333333'@rw   r@   rx   r
   ÚclosedrR   rI   N)ry   ru   rh   rn   r   ri   r   r   ÚSELLr   r{   ÚassertIsNotNoner}   r   r   r   r   Ú;test_wash_trade_naming_a_real_resting_sell_is_still_adoptedž   s   ÿ
zTWashTradeExitBugfixTests.test_wash_trade_naming_a_real_resting_sell_is_still_adoptedc                 C   sp   g | j _| j ¡  | j ¡  | j ¡  |  | j jdd¡ t tj	¡t
dd� | j_| j ¡  |  | j jd¡ dS )z{reconcile_with_broker() must not hit the broker on every call --
        only once per schedule.reconcile_interval_seconds.r!   z<back-to-back calls within the throttle window must be no-opsi'  )Úsecondsé   N)ri   r   rh   Úreconcile_with_brokerr}   r   r   rk   r   rl   r   Ú_last_reconciler    r   r   r   Ú'test_reconcile_with_broker_is_throttled±   s   


ÿ
z@WashTradeExitBugfixTests.test_reconcile_with_broker_is_throttledN)r   r   r   ro   rs   r�   r…   rŠ   r   r   r   r   r)   N   s    /r)   Ú__main__)r(   ÚbuiltinsÚ@py_builtinsÚ_pytest.assertion.rewriteÚ	assertionÚrewriteÚ
@pytest_arÚsysrW   rq   rU   Úunittestr   r   r   r[   Úinsertr\   r]   r^   r   Ú	Exceptionr   r   ÚTestCaser)   r   Úmainr   r   r   r   Ú<module>   s    "&	
tÿ