o
    Ü‡ jÖ˜  ã                   @   sþ   d 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 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ddlZddl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d„ Z e!dkr}e ƒ  dS dS )ax  
monitor.py

Main orchestrator. Runs the full daily lifecycle:

    START
     -> premarket_scanner.py (09:29, one full-universe scan) -> top
        candidates (premarket_20)
     -> start/maintain SIP stream for premarket_20
     -> opening confirmation -> entry (evaluated across premarket_20)
     -> continuous intraday health scoring of premarket_20 (every
        intraday_health.eval_interval_seconds)
     -> full intraday rescan + premarket_20 rotation (every
        intraday_health.full_rescan_interval_minutes, until the
        no-new-entries cutoff)
     -> position management + trailing stops
     -> on exit -> top_stocks.py -> find replacement (from premarket_20)
        -> confirm -> enter
     -> end-of-day liquidation
     -> shutdown

[UPDATED 2026-09-01] Per explicit request, the single scan moved from
09:00 to schedule.premarket_scan_time (09:29) and the 09:00-09:25
development-monitoring window plus the 09:25 final-scoring review were
eliminated entirely -- there's no time left for either between a 09:29
scan and the 09:30 open. final_10 is now populated directly from
premarket_20's top candidates.total_score ranking right after the scan
(see _run_premarket_scan()), with no development-trend adjustment
applied (there's no multi-snapshot history to compute one from anymore).

[FEATURE 2026-08-17] premarket_20 is now the primary intraday candidate
universe (previously this was final_10, which restricted the bot to
only 10 of the original 20 candidates for the entire day). final_10 is
still written for the dashboard's own tab and for backward
compatibility, but no longer gates entries or replacement search.

Run directly:
    python monitor.py

Designed to be started once per day via cron shortly before 09:00 ET,
and to exit cleanly after end-of-day liquidation. A singleton file
lock prevents two instances from ever running against the same account
simultaneously.
é    N)ÚdatetimeÚtimezoneÚ	timedelta)Ú
get_configÚget_envÚvalidate_config)Ú
get_logger)Úevaluate_entry)Úevaluate_entry_pipeline)ÚPositionManager)ÚStreamManager)Ú
get_clientÚmonitorc                   @   sN   e Zd ZdZ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S )Ú_EntryAttemptTrackeraš  
    [FEATURE 2026-09-01] Fixes the shortlist-starvation bug found in the
    2026-09-01 session review -- see config.json's
    intraday_health._note_confirmation_failure_cooldown for the full
    real-world case (F monopolizing a shortlist slot for ~24 minutes
    while CRML, health-eligible and competitively scored, never got a
    single evaluate_entry() call before rotating back out).

    _scan_for_entries() ranks premarket_20 by health_score and only
    evaluates the top entry_shortlist_size symbols each poll cycle. A
    symbol that ranks highly but keeps failing entry_engine's
    confirmation on a condition that doesn't change bar-to-bar (e.g. a
    hard momentum disqualifier) can occupy one of those slots forever,
    since nothing here ever demoted it. This class tracks consecutive
    not-confirmed results per symbol and temporarily benches one after
    max_failures in a row, freeing its shortlist slot for the next-best
    candidate for cooldown_seconds.

    Deliberately does NOT change entry_engine's actual confirmation
    criteria in any way -- a benched symbol that comes off the bench is
    evaluated against the exact same rules as before. This only changes
    WHICH candidates get a turn, never what it takes to pass.

    Pure in-memory, per-session state (like self.premarket_20 itself) --
    intentionally not persisted, since a fresh session should start
    every symbol unbenched.
    Úmax_failuresÚcooldown_secondsc                 C   s   || _ || _i | _i | _d S ©N)r   r   Ú_consecutive_failuresÚ_benched_until)Úselfr   r   © r   úC/var/www/screener/trade/premarket_backup_2026-09-08_2010/monitor.pyÚ__init__a   s   
z_EntryAttemptTracker.__init__ÚsymbolÚnowÚreturnc                 C   s   | j  |¡}|d uo||k S r   )r   Úget)r   r   r   Úuntilr   r   r   Ú
is_benchedg   s   z_EntryAttemptTracker.is_benchedÚ	confirmedc              	   C   s”   |r| j  |d ¡ | j |d ¡ d S | j  |d¡d }|| jkrC|t| jd� | j|< d| j |< t d|› d| jd›d|› d�¡ d S || j |< d S )	Nr   é   )Úsecondsú[ENTRY] z& benched from the entry shortlist for z.0fzs after zE consecutive not-confirmed attempts -- giving other candidates a turn)	r   Úpopr   r   r   r   r   ÚlogÚinfo)r   r   r   r   Úcountr   r   r   Úrecord_resultk   s   

ÿÿz"_EntryAttemptTracker.record_resultN)Ú__name__Ú
__module__Ú__qualname__Ú__doc__ÚintÚfloatr   Ústrr   Úboolr   r'   r   r   r   r   r   D   s
    r   c                   @   s˜   e Zd Zdd„ Zdd„ Zdd„ Zdd„ Zd	d
„ Zdd„ Zde	fdd„Z
dd„ Zdd„ Zdd„ Zdd„ Zdefdd„Zdd„ Zdd„ Zdd „ Zd!d"„ Zd#S )$ÚSessionOrchestratorc                 C   sè   t ƒ | _t| jƒ tƒ  ¡  | jd d dk| _tƒ | _t| jd�| _	t
ƒ | _t| jd d | jd d d�| _t t ¡ ¡}t |¡ g | _g | _g | _i | _d | _d | _d	| _i | _t tj| j¡ t tj| j¡ t  | j!¡ d S )
NÚmodeÚexecution_modeÚ
simulation)r3   Úintraday_healthÚ%max_consecutive_confirmation_failuresÚ%confirmation_failure_cooldown_seconds)r   r   F)"r   Úcfgr   r   Úvalidater3   r   Úclientr   Úposition_mgrr   Ústreamr   Ú_entry_trackerr4   Úprune_stale_health_stateÚ
data_storeÚload_health_stateÚsave_health_stateÚpremarket_20Úfinal_10Ú_prefiltered_universeÚ_opening_volume_baselineÚ_last_health_evalÚ_last_full_rescanÚ	_shutdownÚ_pipeline_stateÚsignalÚSIGTERMÚ_handle_signalÚSIGINTÚatexitÚregisterÚ_cleanup)r   Úpruned_health_stater   r   r   r   }   s0   

þ
zSessionOrchestrator.__init__c                 C   s   t  d|› d�¡ d| _d S )Nz[SHUTDOWN] Received signal z, shutting down gracefullyT)r$   r%   rG   )r   ÚsignumÚframer   r   r   rK   ¥   s   
z"SessionOrchestrator._handle_signalc                 C   s0   z| j  ¡  W n	 ty   Y nw t d¡ d S )Nz[SHUTDOWN] Cleanup complete)r;   ÚstopÚ	Exceptionr$   r%   ©r   r   r   r   rO   ©   s   ÿzSessionOrchestrator._cleanupc                 C   sx   t  d| jd d › �¡ t ¡ st  d¡ d S |  ¡  | jr!d S |  ¡  |  ¡  |  	¡  |  
¡  |  ¡  t  d¡ d S )Nz#[START] monitor.py starting | mode=r1   r2   z[START] Not a weekday, exitingz[SHUTDOWN] Session complete)r$   r%   r7   Úmarket_timeÚ
is_weekdayÚ_wait_for_premarket_scanrG   Ú_run_premarket_scanÚ_start_streamingÚ_wait_for_market_openÚ_main_trading_loopÚ_end_of_day_liquidationrU   r   r   r   Úrun±   s   
zSessionOrchestrator.runc                 C   ó6   t  ¡ s| jst d¡ t  ¡ s| jrd S d S d S d S )Né   )rV   Úis_premarket_scan_timerG   ÚtimeÚsleeprU   r   r   r   rX   Å   ó   
ÿz,SessionOrchestrator._wait_for_premarket_scanc                 C   sÂ   t  | j¡}t dt|ƒ› �¡ t  | j|¡| _t dt| jƒ› d�¡ t j| j	d d | jd�| _
| j
d | j	d d … | _t dd	„ | j
D ƒd
d	„ | jD ƒdœ¡ t | j
¡ |  | j
¡ d S )Nz1[PREMARKET] Universe size after asset filtering: z[PREMARKET] Prefiltered to z symbols in price/volume bandÚ
candidatesÚpremarket_candidate_count)ÚprefilteredÚfinal_candidate_countc                 S   ó   g | ]}|d  |d dœ‘qS ©r   Útotal_score)r   Úscorer   ©Ú.0Úrr   r   r   Ú
<listcomp>å   ó    z;SessionOrchestrator._run_premarket_scan.<locals>.<listcomp>c                 S   ri   rj   r   rm   r   r   r   rp   æ   rq   )rA   rB   )Úpremarket_scannerÚget_universe_symbolsr9   r$   r%   ÚlenÚprefilter_by_snapshotrC   Úscanr7   rA   rB   r>   Úsave_watchlistÚwrite_top_stocksÚ_sync_opening_volume_baseline)r   Úuniverser   r   r   rY   É   s   þþz'SessionOrchestrator._run_premarket_scanre   c                 C   s6   |D ]}|  di ¡}t|  dd¡dƒ| j|d < qdS )aY  Keeps self._opening_volume_baseline current as premarket_20
        gains symbols (initial scan, slot-replacement additions, and
        full-rescan rotations) -- entry_engine's opening-volume-expansion
        check needs a real baseline per symbol, not the accidental "no
        baseline -> ratio always looks huge" behavior of a missing key.ÚmetricsÚpremarket_volumer    r   N)r   ÚmaxrD   )r   re   ro   r{   r   r   r   ry   ë   s   þz1SessionOrchestrator._sync_opening_volume_baselinec                 C   sT   dd„ | j D ƒ}|st d¡ d S | j |¡ | j ¡ r#t d¡ d S t d¡ d S )Nc                 S   ó   g | ]}|d  ‘qS ©r   r   rm   r   r   r   rp   ú   ó    z8SessionOrchestrator._start_streaming.<locals>.<listcomp>z5[OPEN] No candidates to stream; skipping stream startz[OPEN] SIP stream activezF[OPEN] SIP stream failed to become healthy; will rely on REST fallback)rA   r$   Úwarningr;   ÚstartÚ
is_healthyr%   Úerror)r   Úsymbolsr   r   r   rZ   ö   s   

z$SessionOrchestrator._start_streamingc                 C   r_   )Né   )rV   Úis_past_market_openrG   rb   rc   rU   r   r   r   r[     rd   z)SessionOrchestrator._wait_for_market_openc                 C   s  | j d d }| j d }t tj¡}|| _|| _| js‚t 	¡ s„| j
 ¡  |  ¡  | j
 ¡ r5t ¡ s5|  ¡  | j
 ¡  t tj¡}| dd¡rZ|| j  ¡ }||d krZ|  ¡  || _t ¡ st|| j  ¡ }||d d krt|  ¡  || _t |¡ | js†t 	¡ rd S d S d S d S )	NÚscheduleÚpoll_interval_seconds_intradayr4   ÚenabledTÚeval_interval_secondsÚfull_rescan_interval_minutesé<   )r7   r   r   r   ÚutcrE   rF   rG   rV   Úis_force_liquidate_timer:   Úpoll_pending_exitsÚ_update_open_positionsÚhas_available_slotÚis_new_entries_cutoffÚ_scan_for_entriesÚreconcile_with_brokerr   Útotal_secondsÚ_update_intraday_healthÚ_run_intraday_full_rescanrb   rc   )r   ÚintervalÚ
health_cfgr   Úelapsedr   r   r   r\   	  s0   



Üz&SessionOrchestrator._main_trading_loopc                 C   sj   | j  ¡ D ]-}| j |¡}|sq|d d }| j  |||¡}|r2| j  |||¡ t |¡ |  |¡ qd S )NéÿÿÿÿÚc)	r:   Úget_open_symbolsr;   Úget_barsÚupdate_positionÚexit_positionÚ
top_stocksÚregister_cooldownÚ_handle_slot_freed)r   r   ÚbarsÚcurrent_priceÚexit_signalr   r   r   r‘   6  s   

€÷z*SessionOrchestrator._update_open_positionsÚfreed_symbolc                    sl   t  d¡ | j ¡ }tj|d�‰ ˆ sd S | j ˆ d g¡ ‡ fdd„| jD ƒ| _| j 	ˆ ¡ |  
ˆ g¡ d S )Nz [SLOT] 1 position slot available)Úexclude_symbolsr   c                    s    g | ]}|d  ˆ d  kr|‘qS r   r   rm   ©Ú	candidater   r   rp   X  s     z:SessionOrchestrator._handle_slot_freed.<locals>.<listcomp>)r$   r%   r:   rž   r¢   Úfind_replacementr;   Úadd_symbolsrA   Úappendry   )r   r¨   Úopen_symbolsr   rª   r   r¤   B  s   

z&SessionOrchestrator._handle_slot_freedc                 C   sª  t  ¡ }| jd d }t tj¡}g }| jD ]]}|d }| j 	|¡r#q| 
|i ¡}| 
dd¡}t |¡s@t d|› d|› �¡ q| j |¡rPt d|› d�¡ q| j ||¡rat d|› d	�¡ q| 
d
d¡}	| |	| 
dd¡|f¡ q|jdd„ dd� |d |… }
|
r”t dd dd„ |
D ƒ¡ ¡ | jd d }|
D �]4\}	}}|d }| j ¡ s¯ d S | j |¡}| j |¡}| j 
|d¡}|rÊt|ƒdk rÔt d|› d�¡ q�t |||¡}| 
|i ¡ 
dd¡}|j|krÿt d|› d|› d|j› d|jd›d �	¡ | j |¡}| jd!  
d"¡d#k�r„| j |¡}| jd$  
d%d&¡}| j  !|i ¡}t"|||||t# $¡ ||d'�}|j%�rSt &d(|› d)|j'd›d*|j(�rMd+ |j(¡nd,› �¡ n<| j 
d-i ¡ 
d.d/¡}d(|› d0|j'd›d*|j)�rqd+ |j)¡nd,› �}|�r~t &|¡ nt |¡ nt*|||||||d1�}| j +||j%t tj¡¡ |j%�rÒ|d2 d3 }| j,�s¯| j- .¡ nd }| jd4 d5 }|�rÀt/|j0ƒn|}d+ |j(¡}| j 1|||||¡ q�d S )6Nr4   Úentry_shortlist_sizer   ÚstateÚWATCHz[ENTRY] Skipping z	: health=z: stale stream dataz.: benched after repeated confirmation failuresÚhealth_scoreg        rk   c                 S   ó   | d | d fS )Nr   r    r   )Útr   r   r   Ú<lambda>Ž  ó    z7SessionOrchestrator._scan_for_entries.<locals>.<lambda>T©ÚkeyÚreversez[ENTRY] Shortlist this cycle: z, c                 s   s,   � | ]\}}}|d  › d|d›d�V  qdS )r   z(health=ú.1fú)Nr   )rn   ÚhsÚ_ro   r   r   r   Ú	<genexpr>“  s   €* z8SessionOrchestrator._scan_for_entries.<locals>.<genexpr>rz   Úmin_avg_daily_volumer    é   z1: insufficient live bars for a fresh health checkú?r"   z: cached health=z but fresh read=z (score=r»   z0) -- using the fresh read for the entry decisionÚentryÚdecision_engineÚprediction_pipelineÚ	streamingÚsub_minute_bucket_secondsé   )Úsession_elapsed_minutesr±   Úsub_bucket_secondsz[PIPELINE] z confirmed (z): z; zno specific reason recordedÚloggingÚlog_rejectionsFz not confirmed ()Úhealth_readingÚ
is_reentryrœ   r�   ÚtradingÚsimulated_equity_default)2r>   r?   r7   r   r   r   rŽ   rA   r:   Úis_symbol_openr   r4   Úis_eligible_for_entryr$   Údebugr;   Úis_symbol_staler<   r   r®   ÚsortÚjoinr’   rŸ   Ú	get_quoterD   rt   Úcompute_healthÚ	raw_stater³   Úhas_closed_position_todayÚget_bars_subrH   Ú
setdefaultr
   rV   Úminutes_since_openÚshould_enterr%   Úconfirmation_scoreÚreasons_forÚreasons_againstr	   r'   r3   r9   Úget_accountr-   ÚequityÚenter_position)r   Úhealth_stateÚshortlist_sizer   Úeligiblero   r   rÃ   Úconfirmed_stater³   Ú	shortlistÚavg_vol_baselinerk   r¥   ÚquoteÚbaselineÚfresh_readingÚcached_staterÎ   Úbars_subrÊ   Úpipeline_stateÚdecisionrÌ   ÚmessageÚentry_priceÚaccountÚ
sim_equityrã   Úreasonr   r   r   r”   \  s    

ÿ

ÿ
ÿ
ý
ÿÿÿ€°z%SessionOrchestrator._scan_for_entriesc                 C   sr   | j d d }t ¡ }| jD ]#}|d }| j |¡}|r"t|ƒdk r#qt ||||¡\}}|||< qt 	|¡ dS )a<  
        [FEATURE 2026-08-17] Evaluates every symbol currently in
        premarket_20 through intraday_health.evaluate_symbol(), using
        the SIP stream's already-buffered bars (no extra API calls).
        Persists the hysteresis-confirmed state to
        state/intraday_health.json, which _scan_for_entries() and
        top_stocks.find_replacement() both read.

        This function NEVER touches position_manager.py or triggers an
        exit -- an existing open position is left alone regardless of
        what health state its symbol reports here.
        rz   rÀ   r   rÁ   N)
r7   r>   r?   rA   r;   rŸ   rt   r4   Úevaluate_symbolr@   )r   rê   rå   ro   r   r¥   ÚreadingÚ	new_entryr   r   r   r—   ê  s   
ÿ
z+SessionOrchestrator._update_intraday_healthc                 C   s  | j s
t d¡ dS t dt| j ƒ› d�¡ | jd }| jd d }t ¡ }tj	t| j ƒ| j |d d	�}g }|D ]S}|d
 }|dd„ | j
D ƒv rN| j |¡nd}|rXt|ƒdk rh|d |d< d|d< | |¡ q8t ||||¡\}	}
|
||< |	j|d< |	j|d< t |	j¡r‹| |¡ q8t |¡ |sšt d¡ dS |jdd„ dd� |d|d … }dd„ |D ƒ}t| j ¡ ƒ}dd„ |D ƒ}| j
D ]O}|d
 }||v �r||v�r| |¡}|dur÷| |¡ | |¡ t d|› d| dd¡d ›�¡ qÃt d|› d!| dd¡d ›d"�¡ | |¡ | |¡ qÃ|| _
|  |¡ | j d#d„ |D ƒ¡ | j d$i ¡ d%d&¡}|d|… }t ¡ }d'd„ |D ƒ|d(< d)d„ |D ƒ|d*< t |¡ t |¡ |D ]}t d|d
 › d+|d › d,| d¡› �¡ �qYt d-t|ƒ› d.t|ƒ› �¡ dS )/a\  
        [FEATURE 2026-08-17] Every intraday_health.full_rescan_interval_
        minutes (default 30), re-scores the ENTIRE cached prefiltered
        universe (not just the current premarket_20) against live,
        current bars -- using the same scoring function and the same
        health-eligibility filter as the rest of the intraday system --
        and rotates premarket_20 to the resulting top N. This is what
        lets the bot discover a genuinely new candidate that wasn't
        part of the original 09:00 list, not just rotate among the
        original 20 all day.

        Reuses self._prefiltered_universe (captured once at 09:00)
        rather than re-pulling the full tradable-asset list and a fresh
        bulk snapshot every 30 minutes.

        Any symbol with an open position is always kept in the pool
        regardless of whether the fresh rescan re-selects it -- dropping
        it here would only affect future entry/replacement eligibility
        bookkeeping, never the open position itself, but keeping it
        visible avoids losing track of it in logs/dashboard.
        zD[INTRADAY-RESCAN] No cached prefiltered universe available; skippingNz*[INTRADAY-RESCAN] Starting full rescan of z prefiltered symbolsr4   rz   rÀ   Úfull_rescan_lookback_hours)Úcandidate_countrg   Úlookback_hoursr   c                 S   r~   r   r   )rn   Úpr   r   r   rp   .  r€   zASessionOrchestrator._run_intraday_full_rescan.<locals>.<listcomp>rÁ   rk   r³   r²   rå   zb[INTRADAY-RESCAN] No health-eligible candidates found; keeping the existing premarket_20 unchangedc                 S   r´   )Nr³   rk   r   )ro   r   r   r   r¶   I  r·   z?SessionOrchestrator._run_intraday_full_rescan.<locals>.<lambda>Tr¸   Úfull_rescan_pool_sizec                 S   s   i | ]}|d  |“qS r   r   rm   r   r   r   Ú
<dictcomp>m  s    zASessionOrchestrator._run_intraday_full_rescan.<locals>.<dictcomp>c                 S   s   h | ]}|d  ’qS r   r   rm   r   r   r   Ú	<setcomp>o  r€   z@SessionOrchestrator._run_intraday_full_rescan.<locals>.<setcomp>z[INTRADAY-RESCAN] z6 kept in pool (open position) with refreshed pm_high=$Úpm_highr   ú.2fz` kept in pool (open position) but missing from this rescan's universe -- reusing stale pm_high=$zj; resistance will not reflect current price action until this symbol reappears in the prefiltered universec                 S   r~   r   r   rm   r   r   r   rp   „  r€   re   rh   é   c                 S   ó&   g | ]}|d  |d |  d¡dœ‘qS ©r   rk   rå   )r   rl   Úhealth©r   rm   r   r   r   rp   —  ó    ÿÿrA   c                 S   r  r  r  rm   r   r   r   rp   ›  r  rB   z score=z health=z*[INTRADAY-RESCAN] Rotated premarket_20 -> z candidates, final_10 -> top ) rC   r$   r�   r%   rt   r7   r>   r?   rr   rv   rA   r;   rŸ   r®   r4   r÷   r³   rè   rÒ   r@   rÕ   Úsetr:   rž   r   ÚaddrÓ   ry   r­   Úload_watchlistrw   rx   )r   r7   rê   rå   Úrescoredrç   ro   r   r¥   rø   rù   Únew_poolÚrescored_by_symbolr¯   Úpool_symbolsÚfreshÚfinal_countÚfinal_sliceÚ	watchlistr   r   r   r˜     sœ   

ÿ
ý$
ÿ


€

#




ÿ
þ

€

þ
þ

ÿÿz-SessionOrchestrator._run_intraday_full_rescanc           	      C   sV  t  d¡ | jjdd� | j di ¡ dd¡}| jd d }t ¡ | }t ¡ |k rN| j ¡  | j ¡ | j 	¡  }|s<n0t 
|¡ | jjdd� t ¡ |k s*| j ¡ | j 	¡  }|rlt  dt|ƒ› d	|› d
t|ƒ› �¡ | j ¡  t ¡ }t |¡ tdd„ |D ƒƒ}tdd„ |D ƒƒ}tdd„ |D ƒƒ}t  dt|ƒ› d|› d|› d|d›�¡ dS )a]  
        [BUGFIX 2026-08-24] Previously this called liquidate_all() ONCE,
        stopped the stream, and logged the summary immediately -- with
        no retry window at all. Any position whose close attempt failed
        on that single pass (e.g. the "position not found" entry-
        settlement race the same day's fix addresses in
        position_manager.py, or a wash-trade rejection needing a poll
        cycle to resolve) was simply left "open"/"closing" in the local
        ledger, EXCLUDED from that day's trades/wins/losses/total_P/L
        summary, and only discovered -- and only actually logged as a
        completed trade -- by the NEXT day's startup reconciliation.
        Confirmed in real logs: 2026-08-21's 5 stuck positions all
        surfaced as EXIT_REASON=END_OF_DAY entries in the 2026-08-24
        log, at market open, a full trading day later than they should
        have resolved.
        Fix: keep calling liquidate_all() (idempotent -- see
        exit_position()) and poll_pending_exits() every poll interval
        for up to eod_liquidation_max_wait_seconds, so anything that
        just needs a few more cycles to settle actually finishes THIS
        session and gets counted THIS day's summary. Only genuinely
        stuck stragglers (which should now be rare) still fall through
        to next-day reconciliation.
        z*[EOD] Force-liquidating all open positionsÚ
END_OF_DAY)rö   rˆ   Ú eod_liquidation_max_wait_secondséx   r‰   z[EOD] z& position(s) still not resolved after zLs of retries -- will fall through to next session's startup reconciliation: c                 s   s*   � | ]}|  d ¡dkr|  dd¡V  qdS )ÚstatusÚclosedÚ
current_plr   Nr  ©rn   rµ   r   r   r   r¿   é  s   €( z>SessionOrchestrator._end_of_day_liquidation.<locals>.<genexpr>c                 s   s2   � | ]}|  d ¡dkr|  dd¡dkrdV  qdS ©r  r  r  r   r    Nr  r  r   r   r   r¿   ê  ó   €0 c                 s   s2   � | ]}|  d ¡dkr|  dd¡dkrdV  qdS r  r  r  r   r   r   r¿   ë  r  z[EOD] Session summary: trades=z wins=z losses=z total_P/L=$r  N)r$   r%   r:   Úliquidate_allr7   r   rb   r�   rž   Úget_closing_symbolsrc   r�   rt   Úsortedr;   rS   r>   Úload_today_tradesÚwrite_trades_summaryÚsum)	r   Úmax_waitÚpoll_intervalÚdeadlineÚ
still_openÚtradesÚtotal_plÚwinsÚlossesr   r   r   r]   ©  s:   


õÿþ

ÿz+SessionOrchestrator._end_of_day_liquidationN)r(   r)   r*   r   rK   rO   r^   rX   rY   Úlistry   rZ   r[   r\   r‘   r.   r¤   r”   r—   r˜   r]   r   r   r   r   r0   |   s&    ("-  %r0   c               
   C   sˆ   z$t  ¡ � t  ¡  tƒ } |  ¡  W d   ƒ W d S 1 sw   Y  W d S  tyC } zt t|ƒ¡ t	 
d¡ W Y d }~d S d }~ww )Nr    )r>   Úacquire_singleton_lockÚensure_dirsr0   r^   ÚRuntimeErrorr$   r„   r.   ÚsysÚexit)ÚorchestratorÚer   r   r   Úmainð  s   

&ý€þr3  Ú__main__)"r+   r/  rb   rI   rM   r   r   r   Úconfig_loaderr   r   r   Úlogger_setupr   rV   r>   rr   r¢   r4   Úentry_enginer	   rÅ   r
   Úposition_managerr   r;   r   Úalpaca_clientr   r$   r   r0   r3  r(   r   r   r   r   Ú<module>   s:    -8    x
ÿ