a
    ¦í»j�² ã                   @   s>  d Z ddl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 ej	j
ddd� ddlmZ ddlmZ dd	lmZ ddlZd
ZdZdZedƒZedƒZdZdZedƒZedƒZdZdd„ ZedƒZe dd¡Ze dd¡Z e dd¡Z!edƒZ"edƒZ#edƒZ$dZ%dZ&dZ'dZ(dZ)dZ*dZ+d d!d"œZ,d#Z-d$Z.d%Z/d&Z0d'Z1d(Z2d)Z3h d*£Z4d+Z5d,d-„ Z6da7d†d.d/„Z8d0d1„ Z9d2d3„ Z:d4d5„ Z;d6d7„ Z<d‡d8d9„Z=d:d;„ Z>d<d=„ Z?dd>l@mAZAmBZBmCZCmDZD ddlEZEd?ZFd@eiZGdAdB„ ZHdCdD„ ZIdEdF„ ZJedGƒZKdaLdHdI„ ZMdJdK„ ZNdLdM„ ZOi ZPdNZQdOZRdˆdPdQ„ZSd‰dRdS„ZTdTZUi ZVdUZWdZXdVZYi ZZdWdX„ Z[dŠdYdZ„Z\d[d\„ Z]d]d^„ Z^d_d`„ Z_dadb„ Z`dcdd„ Zadedf„ ZbdgZcdhZddidj„ Zedkdl„ Zfg dm¢Zgdndo„ Zhdpdq„ Zidrds„ Zjdtdu„ Zkdvdw„ Zldxdy„ Zmdzd{„ Znd‹d|d}„Zod~d„ Zpeqd€k�r:ddlrZrerjsd�d‚�Ztetjudƒeve,ƒe-d„d…� et w¡ \ZxZyexjzZ-epƒ  dS )Œu}  
Live match monitor â€” polls API-Football for scores, detects halftime,
then checks Kalshi for edge.

Watchlist is read from E:\soccer\watchlist.txt and auto-reloads whenever
the file changes â€” no restart needed. Just edit the file and save.

Format: home | away | league | kickoff (HH:MM 24h)  [| fixture_id]
Lines starting with # are ignored.

Usage:
  python live_monitor.py
é    N)ÚdateÚdatetimeúutf-8Úreplace)ÚencodingÚerrors)ÚPath)ÚMIMEMultipart)ÚMIMETextZ 81b487be73030f5dec980d2a1ef2c406é<   i,  zE:\soccer\live_dataz!E:\soccer\data\clv_kalshi_log.csvéX  zE:\soccer\data\watchlist.txtzE:\soccer\RECYCLE_MONITORgš™™™™™©?c                 C   sl   i }zPt | ƒD ]B}| ¡ }|r| d¡sd|v r| dd¡\}}| ¡ || ¡ < qW n tyf   Y n0 |S )Nú#ú=é   )ÚopenÚstripÚ
startswithÚsplitÚ	Exception)ÚpathÚenvÚlineÚkÚv© r   úlive_monitor.pyÚ	_load_env7   s    r   zE:\soccer\.envZEMAIL_SENDERÚ ÚEMAIL_RECIPIENTÚEMAIL_APP_PASSWORDzE:\soccer\data\bankroll.jsonz#E:\soccer\data\kalshi_bankroll.jsonz!E:\soccer\data\poly_bankroll.jsong¸…ëQ¸¾?gš™™™™™¹?g      Ð?zPpin-CLV [5,12)% vs Pinnacle on each book independently (original live strategy).uà   exclusive-low: bet the CHEAPER exchange (Kalshi vs Poly) when it is BOTH below the other book AND below Pinnacle (pin-CLV [5,12)%). Requires BOTH exchanges quoting â€” fires nothing on matches without live Polymarket prices.)ÚbaselineÚxlowr    Znoneg{®Gáz„?g      $@ç      ð?FT>   ZUSLZCH1ZPL1é   c               
   C   sd   t du r`zddlm}  t| ƒtB a W n: ty^ } z"td|› d�ƒ da W Y d}~dS d}~0 0 t S )a×  Leagues a real xlow bet can happen in: the live scope plus the gap set.

    Read from daily_signals rather than duplicated, so the two cannot drift --
    a league added to the scope must not keep being polled at tracking cadence.

    FAILS OPEN. If the import breaks, every league is treated as tradeable and
    we merely spend quota at the old rate. The opposite default would quietly
    coarsen the leagues we actually trade, which is the expensive direction.
    Nr   )ÚAUTO_TRADE_LEAGUESz"  [odds] cannot read trade scope (z&) -- polling every league at full rate)Ú_TRADEABLE_CACHEÚdaily_signalsr$   ÚsetÚGAP_LEAGUESr   Úprint)r$   Úer   r   r   Ú_tradeable_leaguesÆ   s    r+   c                    s0  |pt }|dkrtnt‰|dkr$tnt‰ t|  d|› �¡p<dƒ}t|  d|› �¡pTdƒ}t|  d|› �¡pldƒ}‡ ‡fdd„}|dk�r|  d	¡tv �rbt|  d
|› d�¡p®dƒ}t|  d|› d�¡pÈdƒ}t|  d|› �¡pàdƒ}	d|  k rúdk �rn nd|  k �rdk �sn dS ||k �o8||ƒ�o8||	ƒ }
||k �oV||	ƒ�oV||ƒ }d|
|fS t|  d
|› d�¡�pzdƒ}t|  d|› d�¡�p–dƒ}d|  k �r²dk �rÐn nd|  k �rÎdk �sÔn dS ||k �oð||ƒ�oð||ƒ }
||k �o||ƒ�o||ƒ }d|
|fS ||ƒ||ƒ||ƒfS )zäWhich books are eligible to bet on `side` under `strategy` (default
    ACTIVE_STRATEGY). Returns (b365_ok, kalshi_ok, poly_ok). Pre-match snapshot
    has already run compute_clv(), so clv_* and *_mid keys are present on `row`.ÚawayZ	clv_b365_r   Úclv_kalshi_Ú	clv_poly_c                    s   ˆ|   koˆ k S   S ©Nr   )Úc©ÚhiÚlor   r   Ú<lambda>è   ó    z"_side_book_flags.<locals>.<lambda>r!   ÚleagueÚkalshi_Ú_midÚpolyus_Úclv_polyus_r   )FFFFÚpoly_)ÚACTIVE_STRATEGYÚEDGE_THRESH_AWAYÚEDGE_THRESH_HOMEÚEDGE_UPPER_CAP_AWAYÚEDGE_UPPER_CAP_HOMEÚfloatÚgetr(   )ÚrowÚsideÚstrategyZclv_bZclv_kZclv_pZinbandÚk_midZpu_midZclv_puZ	kalshi_okZ	polyus_okZp_midZpoly_okr   r1   r   Ú_side_book_flagsÞ   s2    
4
6
rG   c                   C   s.   zt  t ¡ ¡d W S  ty(   Y d S 0 d S ©NÚbankroll)ÚjsonÚloadsÚBANKROLL_FILEÚ	read_textr   r   r   r   r   Ú_load_bankroll  s    rN   c                   C   s.   zt  t ¡ ¡d W S  ty(   Y d S 0 d S rH   )rJ   rK   ÚKALSHI_BANKROLL_FILErM   r   r   r   r   r   Ú_load_kalshi_bankroll  s    rP   c                   C   s.   zt  t ¡ ¡d W S  ty(   Y d S 0 d S rH   )rJ   rK   ÚPOLY_BANKROLL_FILErM   r   r   r   r   r   Ú_load_poly_bankroll  s    rR   c              
   C   sØ   t rtstdƒ d S zŒtdƒ}| |d< t |d< t|d< | t|dƒ¡ t dd¡�.}| 	t t¡ | 
t t| ¡ ¡ W d   ƒ n1 s†0    Y  td	| › �ƒ W n2 tyÒ } ztd
|› �ƒ W Y d }~n
d }~0 0 d S )Nu&     [EMAIL] Not configured â€” skipping.ZalternativeZSubjectZFromZToÚhtmlzsmtp.gmail.comiÑ  z  [EMAIL] Sent: z  [EMAIL] FAILED: )ÚEMAIL_ADDRESSr   r)   r	   r   Zattachr
   ÚsmtplibZSMTP_SSLZloginZsendmailZ	as_stringr   )ÚsubjectZ	html_bodyÚmsgZsrvr*   r   r   r   Ú
send_email  s    0rX   c           +         sä  t ƒ ‰tƒ ‰tƒ ‰d[‡‡‡fdd„	‰dd„ }|p2t‰d\‡‡‡‡‡fdd„	}| d¡pVd	}| d
¡pdd	}| d¡prd	}	| d¡p€d	}
| d¡pŽd	}| d¡pœd	}| d¡pªd	}| d¡tv }|rÜ| d¡pÊd	}| d¡pØd	}n| d¡pèd	}| d¡pöd	}g }t|d|ƒ\}}}|�s |�s |�r¤| d¡�p.d	}t| d¡�p@d	ƒ�pH|}|�r|t| d¡�p`d	ƒ�pxt| d¡�pvd	ƒ}n*t| d¡�pŒd	ƒ�p¤t| d¡�p¢d	ƒ}|�rÂ||	| d¡�p¾d	ƒnd}|�rÞ||	||ƒdd�nd}|�rüt| d¡�pød	ƒnt| d¡�pd	ƒ}|�r`|d	k�r`|| tk�r`t	d ˆd! › d"ˆd › d#|d$›d%|d$›d&�	ƒ d}|�rx||	||ƒdd'�nd}| 
d(|||	| d¡|||d|||f¡ t|d!|ƒ\}}}|�sÈ|�sÈ|�rL| d)¡�pÖd	}t| d*¡�pèd	ƒ�pð|}|�r$t| d+¡�pd	ƒ�p t| d,¡�pd	ƒ}n*t| d-¡�p4d	ƒ�pLt| d.¡�pJd	ƒ}|�rj||| d/¡�pfd	ƒnd}|�r†||||ƒdd�nd}|�r¤t| d,¡�p d	ƒnt| d.¡�p´d	ƒ}|�r|d	k�r|| tk�rt	d ˆd! › d"ˆd › d0|d$›d%|d$›d&�	ƒ d}|�r ||||ƒdd'�nd}| 
d1|
||| d/¡|||d|||f¡ d2d3„ |D ƒ}|�sddS d4d3„ |D ƒ}t|ƒd5k�rd6d7„ |D ƒd(d1hk�rd8d9„ } t|| d:�‰t‡fd;d<„|D ƒƒ}!t	d=ˆd	 › d>| ˆƒd? d@›dA|!d	 › d>| |!ƒd? d@›dB�	ƒ ˆg}|�rfzDt tdCƒ ¡ ¡}"dDd7„ |"D ƒ‰d(d1dEœ‰ ‡ ‡‡fdFd3„|D ƒ}W n t�yd   Y n0 |�spdS dG}#t| d¡�p„d	ƒ}$tdHd<„ |D ƒƒ�rè|$|#k�rè|| }%dI|$ }&| 
dJd	d	|%dt|&dKƒdˆ|%||&ƒƒ|$d	ddf¡ t| d)¡�pød	ƒ}'tdLd<„ |D ƒƒ�r\|'|#k�r\|	| }%dI|' }&| 
dMd	d	|%dt|&dKƒdˆ|%||&ƒƒ|'d	ddf¡ ˆ dNdO¡}(tˆdP dQƒ�r†ˆdP  dR¡n
tˆdP ƒ})| dS¡�rÆdT| dS¡› dU| dV¡› dW| dX¡› �ndY}*ˆ||||(|)|*ˆˆˆdZœ
S )]zGCompute CLV edges for one match. Returns alert dict or None if no edge.Fc                    st   |rˆn
|rˆnˆ }| r |r |s$d S |d }t | | d|   | dƒ}tt|t tƒ| dƒ}|dkrpt |dƒS d S )Nr   r   é   r"   )ÚmaxÚroundÚminÚ
KELLY_MULTÚMAX_BET_PCT)ÚpfÚoddsÚ
use_kalshiÚuse_polyÚbrÚbÚfÚstake)rI   Úkalshi_bankrollÚpoly_bankrollr   r   Ú_kelly_stake4  s    z&_build_clv_alert.<locals>._kelly_stakec                 S   s   | r| dkrd|  S dS )Nr   r   r   )Úmidr   r   r   Ú_kalshi_odds=  s    z&_build_clv_alert.<locals>._kalshi_oddsc                    sN   ˆdkr>t d urt S |rˆn
|r$ˆnˆ}|r:tt| dƒS tS ˆ | |||d�S )Nr!   rY   )ra   rb   )ÚXLOW_STAKE_FIXEDr[   ÚXLOW_STAKE_PCTÚXLOW_STAKE_FALLBACK)r_   r`   ra   rb   rc   )ri   Ú_stratrI   rg   rh   r   r   Ú_stakeB  s    z _build_clv_alert.<locals>._stakeÚpin_fair_homer   Úpin_fair_drawÚpin_fair_awayÚclv_kalshi_homeÚclv_kalshi_awayÚclv_b365_homeÚclv_b365_awayr6   Úclv_polyus_homeÚclv_polyus_awayÚclv_poly_homeÚclv_poly_awayr,   Úkalshi_away_midÚkalshi_away_askÚpolyus_away_askÚpolyus_away_midÚpoly_away_askÚpoly_away_midÚ	b365_awayNT)ra   z  [SPREAD] Úhomeú vs z away: confirming ask ú.3fz vs mid zG -- the mid fired on liquidity that is not there; dropping the poly leg)rb   úAWAY YESÚkalshi_home_midÚkalshi_home_askÚpolyus_home_askÚpolyus_home_midÚpoly_home_askÚpoly_home_midÚ	b365_homez home: confirming ask úHOME YESc                 S   s4   g | ],}|d  dus,|d dus,|d dur|‘qS )r#   Né   é   r   ©Ú.0r*   r   r   r   Ú
<listcomp>‡  r5   z$_build_clv_alert.<locals>.<listcomp>c                 S   s   g | ]}|d    d¡s|‘qS ©r   ÚNO©r   r‘   r   r   r   r“   �  r5   rY   c                 S   s   h | ]}|d  ’qS )r   r   r‘   r   r   r   Ú	<setcomp>‘  r5   z#_build_clv_alert.<locals>.<setcomp>c                 S   s$   t | d pd| d pd| d p dƒS )Nr   r   rY   é	   )rZ   )r*   r   r   r   Ú_clv_max’  s    z"_build_clv_alert.<locals>._clv_max©Úkeyc                 3   s"   | ]}|d  ˆ d  kr|V  qdS )r   Nr   r‘   )Úwinnerr   r   Ú	<genexpr>–  r5   z#_build_clv_alert.<locals>.<genexpr>u(     [DEDUPE] both sides fired â€” keeping z (CLV éd   ú+.1fz%) over ú%)ú E:\soccer\data\pending_bets.jsonc                 S   s(   h | ] }|  d ¡|  d¡|  d¡f’qS )rƒ   r,   Úbet©rB   ©r’   rd   r   r   r   r—   ©  r5   ©rŽ   r†   c                    sJ   g | ]B}ˆd  ˆd |d fˆvrˆd  ˆd ˆ   |d ¡fˆvr|‘qS )rƒ   r,   r   r£   r‘   )Ú_OPPÚ_pkeysÚwr   r   r“   ®  s    þg333333ã?c                 s   s   | ]}|d  dkV  qdS )r   rŽ   Nr   r‘   r   r   r   r�   ¸  r5   r   zNO AWAYé   c                 s   s   | ]}|d  dkV  qdS )r   r†   Nr   r‘   r   r   r   r�   ½  r5   zNO HOMEÚlcr   ÚkickoffÚstrftimez%H:%M ETÚpin_homezH z  D Úpin_drawz  A Úpin_awayúN/A)
r¨   rC   Ú
mins_to_koÚedgesr6   Úko_strÚpin_strrI   rg   rh   )FF)FF)rN   rP   rR   r<   rB   r(   rG   rA   ÚMAX_ASK_MID_SPREADr)   ÚappendÚlenrZ   ÚnextrJ   rK   r   rM   r   Úanyr[   Úhasattrr¬   Ústr)+r¨   rC   r±   rE   Úgate_pendingrk   rp   ÚpfhÚpfdÚpfaZclv_khZclv_kaZclv_bhZclv_baZ_is_gapZclv_phZclv_par²   Zaway_b365_okZaway_kalshi_okZaway_poly_okrF   Zk_askÚp_askÚ
b365_stakeÚkalshi_stakeZ_p_midÚ
poly_stakeZhome_b365_okZhome_kalshi_okZhome_poly_okÚ	yes_sidesr™   ZloserZ_pendZ	NO_THRESHZ
k_away_midZp_noÚpriceZ
k_home_midr6   r³   r´   r   )	r¦   ri   r§   ro   rI   rg   rh   r¨   rœ   r   Ú_build_clv_alert.  sÖ    	,* .ÿÿ
(,* .ÿÿ
(&ÿÿþ

22*ÿ,ÿürÆ   c           1         s 	  | d | d | d   ‰}}| d }t dƒ}z| ¡ rDt | ¡ ¡ng }W n ty`   g }Y n0 ˆ dt ¡ ¡}ddd	œ}i }d
d„ ‰ z4t t dƒjdd�¡}	t	|	t
ƒr®|	n
|	 d|	¡}	W n tyÒ   g }	Y n0 ‡ fdd„|	D ƒ}
|D �]š}|\‰}}}}}}}}}}}t|ƒˆd ˆd ˆ ˆƒf|
v �rTtdˆd › dˆd › dˆ› d�ƒ qêt‡ ‡‡fdd„|D ƒƒ}|�rvqê| ˆ¡‰ˆ�rÌt‡ ‡‡fdd„|D ƒƒ�rÌtdˆ› dˆd › dˆd › dˆ› �ƒ qêˆ d¡}t|ƒˆd ˆd t |¡�pö|||tv �rdntˆˆ dd ¡t|d!ƒ|�s0t|d" d#ƒnd$|�sFt|d" d#ƒnd$|�s\t|d" d#ƒnd$t ¡  d%¡|�rzt|d!ƒnd$|�rŒt|d!ƒnd$d&œ}|�rª||d'< ||d(< |�rìd)ˆv �r¾dnd}|�rÌ|n| d*|› d+�d ¡|d,< ||d-< |�r|�rt|d!ƒn||d.< ||d/< |�rqêd$d$d0œ}|�s6|�s6|�	r~| |¡ td1ˆd › dˆd › dˆ› �|�rpd2|d3›�nd  |�r†d4|d3›�nd  |�rœd5|d3›�nd  ƒ t|ƒ}t|ƒ�oº| }|�rˆd6}td7k�r¬t�r¬d8d6i|d9< z\d:d$l}| |¡} | �r<| d; |d<< | d= |d>< | d? |d@< dA| d; | dB dCœ|d9< dA}W nH t�y† } z.tdDˆd › dˆd › dE|› �ƒ W Y d$}~n
d$}~0 0 |�s¬tdFˆd › dˆd › dG�ƒ d8d6i|dH< z‚d:dIlm}!m}" |"�r8|�s8|!|ƒ} | �r8| d; |dJ< | dK |dL< | dM |dN< | dO |dP< dA| d; | dM | dO  dQ dCœ|dH< W nH t�y‚ } z.tdRˆd › dˆd › dE|› �ƒ W Y d$}~n
d$}~0 0 �nö|�	r~d)ˆv �rœdnd}t|�pªd:ƒ}#d$}$d6}%zZd:d$l}&|&  |ˆd ˆd |¡}'|'�rdA}%|& !|'dS ¡}(|(�r|( dT¡�rt|(dT ƒ}$W n4 t�yD } ztdU|› �ƒ W Y d$}~n
d$}~0 0 t| d*|› d+�¡�p^d:ƒ�p„t| d*|› dV�¡�p|d:ƒ�p„d$})g }*|$�r°|#�r°|* d9t"|$|# ƒ|$f¡ n4|%�rät�rä|* dW¡ tdXˆd › dˆd › dY�ƒ |)�r|#�r|* dHt"|)|# ƒ|)f¡ |*j#dZd[„ d\� t�r.|*j#d]d[„ d\� t$�rBd^d_„ |*D ƒ}*|*�r‚td`|#da›db�dc %ddd„ |*D ƒ¡ de|*d: d: › df� ƒ d6}+|*D �]ð\},}-}.|+�r¢ �	q~|,d9k�rbd8d6i|d9< z\d:d$l}| |¡} | �r| d; |d<< | d= |d>< | d? |d@< dA| d; | dB dCœ|d9< dA}+W nH t�y\ } z.tdDˆd › dˆd › dE|› �ƒ W Y d$}~n
d$}~0 0 �qŠt&ƒ }/t'd$u�rvt'n|/�rŠtt(|/ dgƒnt)}0t|.d!ƒ|d,< |0|d-< d8d6i|dH< z€d:dIlm}!m}" |"�	r.|!|ƒ} | �	r.| d; |dJ< | dK |dL< | dM |dN< | dO |dP< dA| d; | dM | dO  dQ dCœ|dH< dA}+W nH t�	yx } z.tdRˆd › dˆd › dE|› �ƒ W Y d$}~n
d$}~0 0 �qŠ||ˆ< qê| *tj+|dgdh�¡ |S )ia×  Log bets to pending_bets.json for a single alert dict from _build_clv_alert.
    Returns {side: {'kalshi': status_or_None, 'polyus': status_or_None}} so the
    caller can report real trade outcomes in the alert email -- a status dict
    means an order was ATTEMPTED (kill-switch/enabled gates already passed);
    'placed': True carries order_id/amount, 'placed': False means it attempted
    but was skipped/failed downstream (excluded league, no market, drift, etc.).r¨   rC   r²   r6   r¡   Ú	game_dater†   rŽ   r¥   c                 S   s:   | pd  ¡  ¡ } |  d¡r| S d| v r*dS d| v r6dS | S )a¸  Collapse the two strategies' side labels to a common key. The baseline
        path (daily_signals.add_to_pending) writes 'Away'/'Home' while xlow writes
        'AWAY YES'/'HOME YES' -- an exact-string dedupe therefore saw them as
        different bets and let BOTH log, double-staking the same outcome across
        venues (Santos vs Chapecoense 2026-07-25: baseline $0.96 on Kalshi AND
        xlow $1.00 on Poly-US). Found 2026-07-24.r   zNO ZAWAYÚHOME)r   Úupperr   ©Úsr   r   r   Ú
_norm_sideá  s    
z%_log_pending_bets.<locals>._norm_sidezE:\soccer\data\bet_history.jsonr   ©r   Úbetsc              	      s@   h | ]8}t | d ¡ƒdd… | d¡| d¡ˆ | d¡ƒf’qS )r   Né
   rƒ   r,   r¢   )r»   rB   r¤   )rÌ   r   r   r—   ü  s   ÿ$ÿz$_log_pending_bets.<locals>.<setcomp>rƒ   r,   z  [DEDUPE] r„   ú z" already settled -- not re-loggingc                 3   sH   | ]@}|  d ¡ˆd  ko>|  d¡ˆd ko>ˆ |  d¡ƒˆ ˆƒkV  qdS ©rƒ   r,   r¢   Nr£   r¤   )rÌ   rD   r¨   r   r   r�     s   þ$z$_log_pending_bets.<locals>.<genexpr>c                 3   sH   | ]@}|  d ¡ˆd  ko>|  d¡ˆd ko>ˆ |  d¡ƒˆ ˆƒkV  qdS rÑ   r£   r¤   )rÌ   Úopp_sider¨   r   r   r�     s   þ$z already pending for u    â€” keeping first, skipping r•   Zxlow_polyusÚfidr   é   rž   r   Nú%Y-%m-%d %H:%M:%S)r   rƒ   r,   r6   ZdivrE   r¢   Ú
fixture_idZpin_fairZ
clv_kalshiZclv_b365Zclv_polyZentry_tsZentry_k_midZentry_p_midÚb365rf   rÈ   r7   Z_askZ
kalshi_askrÂ   Zpoly_askrÃ   )ÚkalshiÚpolyusz  [PENDING] Logged: z B365 $ú.2fz	 Kalshi $z Poly $Fr!   ÚplacedrÙ   r   Úorder_idZauto_polyus_order_idZquantityZauto_polyus_qtyrÅ   Zauto_polyus_priceTZdollar_committed)rÛ   rÜ   Úamountz!  [AUTO-POLYUS] hook crashed for ú: z!  [AUTO] Poly-US could not place z -- falling back to KalshirØ   )Úplace_auto_orderÚAUTO_TRADE_ENABLEDZauto_order_idZ	placed_atZauto_placed_atÚcountZ
auto_countZprice_centsZauto_price_centsg      Y@z  [AUTO] hook crashed for ZslugZbestAskz%  [AUTO] Poly-US price check failed: r8   )rÙ   ç        râ   z"  [AUTO] Poly-US has a market for z but no BBO -- trying it anywayc                 S   s   | d S ©Nr   r   ©r   r   r   r   r4   Ó  r5   z#_log_pending_bets.<locals>.<lambda>rš   c                 S   s   | d dkrdnd| d fS )Nr   rÙ   r   r   rä   r   r   r   r4   ü  r5   c                 S   s   g | ]}|d  dkr|‘qS )r   rØ   r   ©r’   r   r   r   r   r“      r5   z%_log_pending_bets.<locals>.<listcomp>z  [AUTO] signal(poly.com)=r…   z | ú  c                 s   s:   | ]2\}}}|› d �|r,|d›d|d›d�nd V  qdS )r   r…   z (d=ú)zno-quoteNr   )r’   ÚnÚdZpxr   r   r   r�     s   þ ÿz  -> z
 is closerrY   )Úindent),r   ÚexistsrJ   rK   rM   r   rB   r   ÚtodayÚ
isinstanceÚlistr»   r)   r¹   r   ÚmapsÚleague_kalshi_seriesr(   r<   r[   r   Únowr¬   r¶   ÚboolÚXLOW_PREFER_POLYUSZpoly_us_traderrß   r&   rà   rA   Úpoly_us_pricesZfind_marketZget_bboÚabsÚsortÚXLOW_KALSHI_ONLYÚjoinrP   rl   rm   rn   Z
write_textÚdumps)1ÚalertrC   r²   r6   Zpending_pathÚpendingÚgdZOPPOSITEÚtrade_statusZ_histZsettledr*   Úk_clvÚb_clvr_   r×   rF   rÁ   rÂ   Úopp_midÚp_clvrÀ   rÃ   ÚalreadyZis_noÚentryZside_keyÚstatusZsignal_is_kalshiZsignal_is_polyZ_placed_pusZputZrecrß   rà   Z	signal_pxZ	polyus_pxZpolyus_has_marketZ_pupZ_mktZ_bboZ	kalshi_pxZvenuesrÛ   ZvnameZ_distZvpxZkbZfallback_staker   )rÌ   rÒ   rD   r¨   r   Ú_log_pending_betsÏ  s¬   



ÿ
"$ý
þ

ÿÿÿ

à""

ÿþý
ÿ
ÿÿÿÿÿ
ÿ<$ÿÿ
ÿ
(
þÿü

ÿ
<þÿ
<
r  c           '      C   sÒ  | sdS | d d }| d d }| d   d¡}|r>d|d›�nd}|rRd|d›�nd}|rfd|d›�nd}d	}g }| D �]à}	|	d
 |	d |	d   }
}}|	d |	d |	d   }}}|	  d¡pÀi }d	}|D �]}|\}}}}}}}}}}}}| d¡røqÊdd„ }|  |i ¡} d||   d¡ƒ› d||   d¡ƒ› �}!|d|› d|d d›d|d d›d|d d›d|d›d|�pnd› d|�pzd› d|�p†d› d|�rœd|d › nd› d|�r´d|d › nd› d|�rÌd|d › nd› d|!› d!�7 }qÊd"d#„ |D ƒ}"| |
d$ › d%|
d& › d'd( |"¡› d)�¡ |d*|
d$ › d%|
d& › d+|› d,|› d-t|ƒ› d.|› d/|› d0�7 }qvt| ƒ}#td1k�rpd2nd3}$|$› d'|#› d4|#d5k�rŽd6nd	› d7t ¡  	d8¡› �}%d9|› d:|› d;|› d<|› d=�	}&t
|%|&ƒ dS )>zCSend one email containing all CLV alerts collected this poll cycle.Nr   rI   rg   rh   ú$z,.2fr°   r   r¨   r²   r6   r±   r³   r´   rý   r•   c                 S   s,   | d u rdS |   d¡r(d| d d›d�S dS )Nõ   â€”rÛ   z&<span style='color:#2d7d2d'>&#10003; $rÝ   rÚ   z</span>z-<span style='color:#cc3333'>not placed</span>r£   rÊ   r   r   r   Ú_fmt_legO  s
    
z&send_batch_clv_alert.<locals>._fmt_legzK: rØ   z<br>PolyUS: rÙ   z
        <tr>
          <td><b>z</b></td>
          <td>rž   rŸ   z%</td>
          <td>r…   z</td>
          <td>r  rÚ   z</td>
        </tr>c                 S   s"   g | ]}|d    d¡s|d  ‘qS r”   r–   r‘   r   r   r   r“   h  r5   z(send_batch_clv_alert.<locals>.<listcomp>rƒ   r„   r,   ú (ú, rç   z!
    <h3 style="margin-top:24px">z1</h3>
    <p style="margin:4px 0"><b>League:</b> z &nbsp;|&nbsp; <b>Kickoff:</b> z &nbsp;|&nbsp;
       <b>T-z: min</b></p>
    <p style="margin:4px 0"><b>Pinnacle:</b> a—  </p>
    <table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;margin-bottom:8px">
      <tr style="background:#eee">
        <th>Side</th><th>Kalshi CLV</th><th>B365 CLV</th><th>Poly CLV</th>
        <th>Pin Fair</th><th>B365 Odds</th><th>Kalshi Mid/Price</th><th>Poly Ask/Mid</th><th>B365 Kelly</th><th>Kalshi Kelly</th><th>Poly Kelly</th><th>Real Trade</th>
      </tr>
      z
    </table>r!   z
XLOW ALERTz	CLV ALERTz MATCHr   ZESz) - z%Y%m%d:%H%MzI<html><body style="font-family:sans-serif">
    <p><b>B365 Bankroll:</b> z' &nbsp;|&nbsp; <b>Kalshi Bankroll:</b> z% &nbsp;|&nbsp; <b>Poly Bankroll:</b> z'</p>
    <hr style="margin:8px 0">
    z
    </body></html>)rB   r   r¶   rø   rõ   r·   ÚEMAIL_ALERT_STRATEGYr   rñ   r¬   rX   )'ZalertsrI   rg   rh   Zbr_strZkalshi_br_strZpoly_br_strZmatch_blocksZsubject_partsrú   r¨   r²   r6   r±   r³   r´   Zalert_trade_statusZ	rows_htmlr*   rD   rþ   rÿ   r_   r×   rF   rÁ   rÂ   r   r  rÀ   rÃ   r  Zside_statusZ
trade_cellrÄ   rè   ÚtagrV   rS   r   r   r   Úsend_batch_clv_alert6  sž    


ÿþýüûúùø	÷
öõôó*ÿÿþþýü
ö2ÿÿÿýr  )Úcheck_halftimeÚlist_matchesÚget_all_pricesÚCOMEBACK_RATEz!https://v3.football.api-sports.iozx-apisports-keyc                  C   sd   zJt jt› d�tdd�} |  ¡ }t|ƒr.W dS |d d }|d |d  W S  ty^   Y d	S 0 d
S )a½  Requests left today. 0 when the plan's daily limit is spent.

    API-FOOTBALL SIGNALS EXHAUSTION WITH HTTP 200. It answers every endpoint --
    /status included -- with 200, `response: []` and the reason in an `errors`
    object. So `r.json()['response']['requests']` raised on a list index, the
    bare except caught it, and this returned 9999 ("assume OK") at exactly the
    moment there was no quota left.

    The cost of that fail-open on 2026-08-29: api_ok stayed True, every score
    fetch came back empty, and 161 fixtures were logged status=FT with NO GOALS
    from the clock-based fallback, whose reason string blamed "fixture not
    found" 1,644 times while the real cause was never printed once. Results for
    a whole Saturday did not resolve and the dashboard showed them as open.

    A genuine network failure still returns 9999, because a transient blip
    should not shut off score collection; only a quota answer returns 0.
    z/statusrÏ   )ÚheadersÚtimeoutr   ÚresponseÚrequestsZ	limit_dayZcurrenti'  N)r  rB   ÚAF_BASEÚ
AF_HEADERSrJ   Ú_quota_exhaustedr   )ÚrÚjré   r   r   r   Úget_api_remaining�  s    r  c                 C   s$   | pi   d¡}t|tƒr d|v S dS )z=True when a 200 response is really 'you are out of requests'.r   r  F)rB   rí   Údict)r  Zerrsr   r   r   r  ­  s    
r  c              	   C   sþ   ddl m} t | ¡ ¡}|s"dS dD ]Ò}|||d� }|j|jd fD ]®}tjt› d�t	||| 
¡ dœd	d
�}	|	 ¡  dg ¡D ]t}
|
d d d  ¡ }|
d d d  ¡ }|  ¡ |v sÄ||  ¡ v r€| ¡ |v sÜ|| ¡ v r€|
d d       S q€qHq&dS )z[Search API-Football for a fixture. Tries game_date and game_date+1 to handle UTC/ET offset.r   ©Ú	timedeltaN)r   r   ©Zdaysr   ú	/fixtures)r6   Zseasonr   rÏ   ©r  Úparamsr  r  Zteamsrƒ   Únamer,   ÚfixtureÚid)r   r  rï   Zleague_api_idrÉ   Zyearr  rB   r  r  Ú	isoformatrJ   Úlower)rƒ   r,   Úleague_coderÇ   r  Z	league_idZdeltaZsearch_dateZseason_yearr  ÚfixÚhÚar   r   r   Úfind_fixture_id´  s*    ýür,  zE:\soccer\data\af_cache.sqlitec                  C   sR   t d u rNdd l} tjjddd� | jttƒdd�a t  d¡ t  d¡ t  ¡  t S )Nr   T©ÚparentsÚexist_okg      @)r  zPRAGMA journal_mode=WALzFCREATE TABLE IF NOT EXISTS cache (k TEXT PRIMARY KEY, ts REAL, v TEXT))	Ú_shared_connÚsqlite3Ú
_SHARED_DBÚparentÚmkdirZconnectr»   ÚexecuteÚcommit)r1  r   r   r   Ú
_shared_dbÙ  s    

r7  c                 C   sN   z4t ƒ  d| f¡ ¡ }|r0|d t |d ¡fndW S  tyH   Y dS 0 dS )z2(ts, value) from the cross-process cache, or None.z!SELECT ts, v FROM cache WHERE k=?r   r   N)r7  r5  ZfetchonerJ   rK   r   )r›   rC   r   r   r   Ú_shared_getæ  s
     r8  c                 C   sF   z.t ƒ }| d| t ¡ t |¡f¡ | ¡  W n ty@   Y n0 d S )Nz6INSERT OR REPLACE INTO cache (k, ts, v) VALUES (?,?,?))r7  r5  ÚtimerJ   rù   r6  r   )r›   ÚvalueZdbr   r   r   Ú_shared_putï  s    ÿr;  é  )ÚPSTÚCANCÚABDZSUSPZAWDZWOc           	      C   s"  | sdS ddl }|du rtn|}t | ¡}|rJ|  ¡ |d  |k rJ|d S td| › �ƒ}|rŒ|  ¡ |d  |k rŒ|d |d ft| < |d S zZtjt› d�td| idd�}| ¡  d	g ¡}|rà|d  d
¡pÎi  d¡pØi  d¡nd}W n t	yü   d}Y n0 |  ¡ |ft| < t
d| › �|ƒ |S )aV  API-Football short status ('NS','PST',...), or None if unknown.

    Two cache tiers: the in-process dict avoids sqlite on the hot path (both
    quoters call this every 20s), the shared DB avoids the network across
    processes. max_age overrides the default TTL for callers that can accept a
    staler answer -- see fixture_playable.
    Nr   r   zstatus:r   r%  é   r!  r  r$  r  Úshort)r9  Ú_STATUS_TTLÚ_STATUS_CACHErB   r8  r  r  r  rJ   r   r;  )	rÖ   Úmax_ageÚ_tÚttlÚhitÚsharedr  ZrespÚstr   r   r   Úfixture_status  s,    
ÿ.
rJ  c                 C   s   t | |d�tvS )a  False only when we KNOW it is not being played. Unknown -> True, so a
    lookup failure never silently stops us quoting a real match.

    max_age lets a caller accept a staler answer. The quoters now gate on the
    EXCHANGE's own view first (a delisted or non-active market is a stronger,
    free signal) and use this only as a backstop, so they pass a long max_age
    -- postponements are announced hours ahead, and re-asking every 30 min was
    ~2,200 calls/day for a fact that changes maybe twice a week.
    )rD  )rJ  Ú
NOT_PLAYED)rÖ   rD  r   r   r   Úfixture_playable"  s    
rL  i`T  é  i0*  c                 C   s$   t  | d¡}ttdt|dƒ  tƒS )zFNegative-cache TTL for this fixture: 5m doubling per consecutive miss.r   rY   é   )Ú
_ODDS_MISSrB   r\   Ú_ODDS_NEG_TTLÚ_ODDS_NEG_TTL_MAX)rÖ   rè   r   r   r   Ú_neg_ttl|  s    rR  c              
      sþ  ddl ‰ |du rtn|‰|du rr|durrzt|ƒ}W n ttfyN   d}Y n0 |dkrbtd ‰n|dkrrtd ‰|du r¤|r¤tƒ }|dur¤t|ƒ ¡ |vr¤ˆt9 ‰‡ ‡‡fdd„}t	 
ˆ¡}|durÚˆrÚ||ƒrÚ|d	 S ˆ�rPtd
ˆ› �ƒ}|�rPt|d	 tƒ�rPt|d	 ƒdk�rP|d |d	 d |d	 d	 ff}	||	ƒ�rP|	t	ˆ< |	d	 S ˆ�rdtdˆ› �ƒnd}
|
�r.||
d d|
d	 ffƒ�r.|
d	 }t|tƒ�r.| 
d¡�r.| 
d¡�r.| 
d¡�r.d}td
ˆ› �ƒ}|�rt|d	 tƒ�rt|d	 ƒdk�r|d	 d }t ˆd¡ ˆ   ¡ ||fft	ˆ< ||fS �zZtj
t› d�tˆd	dœdd�}i }| ¡  
dg ¡D ]–}	|	 
dg ¡D ]‚}|d  ¡ }d|v �s˜d|v �rt| 
dg ¡D ]N}| 
d¡d	k�r¤dd„ |d D ƒ}| 
d¡| 
d¡| 
d¡d œ||d < �q¤�qt�qdtd!d"„ | ¡ D ƒdƒ}td#d"„ | ¡ D ƒdƒ}|�s8|�rFt ˆd¡ nt 
ˆd¡d	 tˆ< ˆ   ¡ ||fft	ˆ< td
ˆ› �||gƒ ||fW S  t�yø } zTtd$|› �ƒ t 
ˆd¡d	 tˆ< ˆ   ¡ d%ft	ˆ< td
ˆ› �ddgƒ W Y d}~d%S d}~0 0 dS )&a+  Fetch B365 and Pinnacle match-winner odds from API-Football (cached).

    Pass max_age=0 to force a fresh fetch (used by the nightly paths that want
    the closing line rather than a cached one).

    `hours_to_ko` scales the TTL. A fixture sits in a 24h pre-kickoff window and
    at a flat 1h TTL costs 24 calls whether or not anything moves -- and we
    measured (research/xlow_vs_line_age.py) that the Pinnacle line only changes
    about every 5.8 HOURS. The hourly resolution is only actually wanted near
    kickoff, where CLV is decided; twenty hours out it is re-fetching a number
    that will not move. Far out the TTL relaxes, inside 4h it stays hourly, so
    the CLV series keeps its resolution exactly where it reads it. Callers with
    no kickoff information get the old flat behaviour.
    r   Nrâ   rN  rÔ   rY   c                    s:   | d \}}|s|rˆnt ˆtˆƒƒ}ˆ  ¡ | d  |k S )z‘Is this (ts, (b365, pin)) entry still usable? A cached MISS expires
        sooner than a cached quote -- but on a backoff, not a flat 5 minutes.r   r   )r\   rR  r9  )r  rd   ÚpÚlimit©rE  rÖ   rF  r   r   Ú_fresh¦  s    z#fetch_prematch_odds.<locals>._freshr   zodds:zpinodds:r*  ré   r+  z/odds)r$  r¢   r@  r!  r  Z
bookmakersr#  Úbet365ÚpinnaclerÎ   r%  c                 S   s"   i | ]}|d    ¡ t|d ƒ“qS )r:  Zodd)r'  rA   rå   r   r   r   Ú
<dictcomp>ä  s   ÿz'fetch_prematch_odds.<locals>.<dictcomp>Úvaluesrƒ   Zdrawr,   )r*  ré   r+  c                 s   s"   | ]\}}d |  ¡ v r|V  qdS )rW  N©r'  ©r’   r   r   r   r   r   r�   ë  r5   z&fetch_prematch_odds.<locals>.<genexpr>c                 s   s"   | ]\}}d |  ¡ v r|V  qdS )rX  Nr[  r\  r   r   r   r�   ì  r5   z  [odds] Error: ©NN)r9  Ú	_ODDS_TTLrA   Ú	TypeErrorÚ
ValueErrorr+   r»   rÉ   ÚTRACKING_TTL_MULTÚ_ODDS_CACHErB   r8  rí   rî   r·   r  rO  Úpopr  r  r  rJ   r'  r¸   Úitemsr;  r   r)   )rÖ   rD  Úhours_to_kor6   r*  Z_trrV  rG  rH  r  Z_pin_sharedZ_pinZ_b365Z_prevr  ZbooksZbkr#  r¢   Zvalsr×   Úpinr*   r   rU  r   Úfetch_prematch_odds‚  sŠ    


( 
 0(
ÿÿý
rg  c                 C   s:   d|  d|  d|  }d|  | d| | d| | fS rã   r   )r*  ré   r+  Zoverr   r   r   Úremove_margin  s    rh  c                  C   sn  |   d¡sdS t| d t| ƒ|   d¡d�\}}|r6|sXtd| d › d| d › d	�ƒ dS t|d
 |d |d |d
 |d |d gƒsˆdS t|d
 |d |d ƒ\}}}d| d| d|   }}}|d
 | | }	|d | | }
|d | | }g }|	tk�r| d|	d d›d�¡ |
tk�r:| d|
d d›d�¡ |tk�r\| d|d d›d�¡ td| d › d| d › d| d › d�ƒ td|d
 › d|d › d|d › �ƒ td|d›d|d›d|d›�ƒ td|	d d›d|
d d›d|d d›d�ƒ |�rtd d! |¡› d"�ƒ ntd#td d$›d%�ƒ t	 
| d  ¡ ¡}d}|�r¨z*|   d&t ¡ ¡}t||| d | d ƒ}W n4 t�y¦ } ztd'|› �ƒ W Y d}~n
d}~0 0 |�r\td(|  d)d*¡› d|  d+d*¡› d|  d,d*¡› �ƒ d-d)|fd.d+|fd/d,|ffD ]^\}}}|  |¡}|�rü|d0k�rü|| | }td1|› d2|d d›d3|d4›d5|d4›d�	ƒ �qüt ¡  d6¡t|   d&t ¡ ¡ƒ| d | d | d |   d7d8¡|   dd8¡|d
 |d |d |d
 |d |d t|d9ƒt|d9ƒt|d9ƒt|	d9ƒt|
d9ƒt|d9ƒ|�rd! |¡nd8|�r|  d)d8¡nd8|�r,|  d+d8¡nd8|�r@|  d,d8¡nd8|�rT|  d:d8¡nd8|�rh|  d;d8¡nd8|�r||  d<d8¡nd8|�r�|  d=d8¡nd8|�r¤|  d>d8¡nd8|�r¸|  d?d8¡nd8d@œ}t| ¡ ƒ}tdAƒD ]”}znt ¡  }ttdd8dBdC��6}tj||dD�}|�r| ¡  | |¡ W d  ƒ n1 �s20    Y  W  �qjW n t�yd   t  !d¡ Y n0 �qÔdS )EzMRun pre-match CLV + Kalshi price check. Logs to CLV_LOG. Call once per match.rÓ   Nrª   ©re  r6   z  [CLV] rƒ   r„   r,   z$: odds not available on API-Footballr*  ré   r+  r   zHOME+rž   z.1fú%zDRAW+zAWAY+z	
  [CLV] r	  rç   z    B365:     H=z  D=z  A=z    Pin fair: H=rÚ   z    CLV:      H=rŸ   z%  D=z%  A=z    *** EDGE: r
  z ***z%    No CLV edge (all outcomes within ú.0fr    rÇ   z    [Kalshi] Error: z    Kalshi:   H=Zhome_midú?Zdraw_midZaway_midÚHÚDÚAr   z    Kalshi edge rÞ   z%  (Kalshi r…   z vs Pin fair rÕ   r«   r   rÔ   Zhome_bidZdraw_bidZaway_bidZhome_askZdraw_askZaway_ask)Ú	timestamprÇ   r6   rƒ   r,   r«   rÖ   r�   Ú	b365_drawr‚   r­   r®   r¯   rq   rr   rs   Zclv_homeZclv_drawZclv_awayZclv_flagr‡   Úkalshi_draw_midr|   Úkalshi_home_bidÚkalshi_draw_bidÚkalshi_away_bidrˆ   Úkalshi_draw_askr}   é   r   ©Únewliner   ©Ú
fieldnames)"rB   rg  Ú_hours_to_kor)   Úallrh  Ú
CLV_THRESHr¶   rø   rï   rð   rÉ   r   rì   r  r   r   rñ   r¬   r»   r[   rî   ÚkeysÚrangeÚCLV_LOGrë   r   ÚcsvÚ
DictWriterÚwriteheaderÚwriterowÚPermissionErrorr9  Úsleep)r¨   r×   rf  ÚphZpdÚpaZfair_hZfair_dZfair_aZclv_hZclv_dZclv_aÚflagsÚseriesrØ   rü   r*   ZlabelZmid_keyZfair_prj   Zk_edgerC   Z
CLV_FIELDSÚattemptÚwrite_headerre   Zw2r   r   r   Ú	clv_check  s–    
ÿ
,"""(& .$2&
0

ë
*rŽ  c           
      C   s  | si S i }t dt| ƒdƒD ]ø}| ||d … }d dd„ |D ƒ¡}zštjt› d�td|idd	�}| ¡ }t|ƒr„t	d
ƒ |W   S | dg ¡D ]J}|d d }|d d d |d d d |d d |d d dœ||< q�W q t
�y }	 zt	d|	› �ƒ W Y d}	~	qd}	~	0 0 q|S )zVFetch live scores for multiple fixture IDs in one API call. Returns {fid: score_dict}.r   é   ú-c                 s   s   | ]}t |ƒV  qd S r/   )r»   )r’   re   r   r   r   r�   o  r5   z%fetch_scores_batch.<locals>.<genexpr>r   ZidsrÏ   r!  z_  [batch scores] API-Football daily request limit reached -- scores unavailable until it resetsr  r$  r%  r  rA  ÚelapsedZgoalsrƒ   r,   )r  r‘  rƒ   r,   z  [batch scores] Error: N)r€  r·   rø   r  rB   r  r  rJ   r  r)   r   )
ZfidsZresultsÚiÚchunkZids_strr  r  r)  rÓ   r*   r   r   r   Úfetch_scores_batchh  s0    ÿ


ü&r”  c                  C   sP  t  ¡ st ¡ g fS t ¡ } g }tt dd���}|D ]ò}| ¡ }| d¡rtzt |dd…  ¡ ¡} W q4   Y q40 q4|r4| d¡r„q4dd„ | d	¡D ƒ}t	|ƒd
k r¦q4|d |d |d |d f\}}}}d}	t	|ƒd
k�r|d
  ¡ rø|d
  ¡ d nd}
|
 
¡ �rt|
ƒ}	| |||||	| dœ¡ q4W d  ƒ n1 �s>0    Y  | |fS )z8Parse watchlist.txt. Returns (game_date, list of dicts).r   rÍ   zDATE=rw  Nr   c                 S   s   g | ]}|  ¡ ‘qS r   )r   )r’   rS  r   r   r   r“   ™  r5   z'load_watchlist_file.<locals>.<listcomp>ú|rÔ   r   r   rY   r©   r   )rƒ   r,   rª   r«   rÓ   rÇ   )ÚWATCHLIST_Frë   r   rì   r   r   r   Zfromisoformatr   r·   ÚisdigitÚintr¶   )rÇ   Zmatchesre   r   Úpartsrƒ   r,   rª   r«   rÓ   Útokr   r   r   Úload_watchlist_fileˆ  s8    
$	 

ÿ*r›  c                 C   sB   z.t  |t  | d¡ ¡ ¡}t  ¡ |  ¡ d W S    Y dS 0 dS )zXMinutes elapsed since kickoff. Returns None if unparseable, negative if not yet started.ú%H:%Mr   N)r   ZcombineÚstrptimer9  rñ   Útotal_seconds)Úkickoff_strrÇ   Zkor   r   r   Úclock_elapsed_min°  s
    r   c                 C   sN   z4t |  d¡|  dt ¡ ¡ƒ}|du r*dn| d W S  tyH   Y dS 0 dS )zÜHours until this fixture kicks off, or None if unknown/unparseable.

    Only used to relax the odds cache TTL far from kickoff, so an unknown value
    must degrade to the old flat behaviour rather than to a guess.
    r«   rÇ   Ng      N@)r   rB   r   rì   r   )r¨   Zelr   r   r   r|  ¸  s
    r|  rw  é–   c                 C   sT   |   d¡dv rdS t|   d¡|   dt ¡ ¡ƒ}|du r:dS t |  koNtkS   S )ab  Is this watchlist entry near enough to kickoff for a live score to exist?

    Unparseable kickoff -> True, so a bad time string degrades to the old
    always-poll behaviour rather than silently going blind on a live match.
    A fixture already known to be in play is always in window, which covers
    a kickoff that ran late past the POST bound.
    Úlast_status)Ú1HÚ2HÚHTZETZBTÚPZLIVETr«   rÇ   N)rB   r   r   rì   ÚSCORE_WINDOW_PREÚSCORE_WINDOW_POST)r¨   r*   r   r   r   Ú_in_score_windowÊ  s    r©  c                 C   s*   t | |ƒ}|duo(d|  ko$dkS   S )zATrue if we're roughly in the HT window (46-62 min after kickoff).Né.   é>   )r   )rŸ  rÇ   r*   r   r   r   Úis_ht_windowÚ  s    
r¬  )9rp  rÇ   r6   rƒ   r,   r  r‘  Ú
home_goalsÚ
away_goalsr­   r®   r¯   r�   rq  r‚   r‡   rs  rˆ   rr  rt  rv  r|   ru  r}   rŒ   Zpoly_home_bidr‹   Úpoly_draw_midZpoly_draw_bidZpoly_draw_askr�   Zpoly_away_bidr€   rŠ   Zpolyus_home_bidr‰   Úpolyus_draw_midZpolyus_draw_bidZpolyus_draw_askr   Zpolyus_away_bidr~   rq   rr   rs   rv   Zclv_b365_drawrw   rt   Zclv_kalshi_drawru   rz   Zclv_poly_drawr{   rx   Zclv_polyus_drawry   c                 C   sl  �zPt |  d¡pdƒ}t |  d¡p$dƒ}t |  d¡p6dƒ}|�rP|�rP|�rPd| d|  d|  }d| | }d| | }d| | }t|dƒ| d< t|dƒ| d< t|dƒ| d	< d
|fd|fd|ffD ]H\}}	t |  |¡pÞdƒ}
d| }|
rÈ|	rÈd|	 }t|
| | dƒ| |< qÈd|fd|fd|ffD ]R\}}	t |  |¡�p@dƒ}d| d¡d  }|�r(|	�r(t|	| |	 dƒ| |< �q(d|fd|fd|ffD ]R\}}	t |  |¡�pªdƒ}d| d¡d  }|�r’|	�r’t|	| |	 dƒ| |< �q’d|fd|fd|ffD ]R\}}	t |  |¡�pdƒ}d| d¡d  }|�rü|	�rüt|	| |	 dƒ| |< �qüW n t�yf   Y n0 | S )zMCompute pin_fair and CLV columns in-place. Returns row with CLV fields added.r­   r   r®   r¯   r   r#   rq   rr   rs   r�   rq  r‚   Zclv_r‡   rr  r|   r-   Ú_rŒ   r¯  r�   r.   rŠ   r°  r   r:   )rA   rB   r[   r   r   )rC   rˆ  Zpd_r‰  Zovr½   r¾   r¿   Zb_keyr_   rd   Zclv_keyZ	fair_oddsZk_keyr   Zp_keyrS  r   r   r   Úcompute_clvö  sH    r²  c                 C   s8   t | dƒr|  ¡ }nt| ƒ}tjddd� t|› d� S )zPReturn the per-day CSV path for a given game_date (date or 'YYYY-MM-DD' string).r&  Tr-  z.csv)rº   r&  r»   ÚLIVE_LOG_DIRr4  )rÇ   Zgd_strr   r   r   Úlive_log_path!  s
    

r´  c               	      s*  t  ¡ sdS ddl} tt  d¡ƒD �] }t|dddd��}| ¡ }W d  ƒ n1 sV0    Y  | ¡ sjq"t 	|  
|¡¡}|jp‚g ‰ ‡ fdd	„tD ƒ}|sœq"td
|j› d|› �ƒ t|ƒ}t|dddd��H}tj|td�}| ¡  |D ]‰| ‡fdd„tD ƒ¡ qæW d  ƒ q"1 �s0    Y  q"dS )z=Add any missing columns to each per-day live_data CSV header.Nr   z*.csvr  r   r   rx  c                    s   g | ]}|ˆ vr|‘qS r   r   )r’   r0   )Úexistingr   r   r“   6  r5   z$migrate_live_log.<locals>.<listcomp>z  [migrate] Adding columns to rÞ   r¨   rz  c                    s   i | ]}|ˆ   |d ¡“qS ©r   r£   ©r’   r   ©rC   r   r   rY  ?  r5   z$migrate_live_log.<locals>.<dictcomp>)r³  rë   ÚioÚsortedZglobr   Úreadr   r‚  Z
DictReaderÚStringIOr{  Ú
LOG_FIELDSr)   r#  rî   rƒ  r„  r…  )r¹  r   re   ZcontentÚreaderZmissingZrowsr¨   r   )rµ  rC   r   Úmigrate_live_log*  s(    &
r¿  c                 C   sX   zDt  | ¡ ¡}|sW dS t||| |ƒ}|s2W dS dd„ | ¡ D ƒW S    Y dS 0 dS )zXFetch home/draw/away prices from Kalshi. Returns dict with kalshi_ prefix keys, or None.Nc                 S   s   i | ]\}}d |› �|“qS )r7   r   r\  r   r   r   rY  J  r5   z fetch_kalshi.<locals>.<dictcomp>)rï   rð   rÉ   r  rd  )rƒ   r,   r(  rÇ   r‹  Úrawr   r   r   Úfetch_kalshiA  s    rÁ  c                 C   sN   z4t j|| || ¡ d�}|s"W dS dd„ | ¡ D ƒW S  tyH   Y dS 0 dS )z£Fetch home/draw/away prices from Polymarket. Returns poly_-prefixed dict, or None.
    Mid/bid/ask, same shape as fetch_kalshi. Cheap single-tag fetch via league=.©r6   Nc                 S   s   i | ]\}}d |› �|“qS )r;   r   r\  r   r   r   rY  V  r5   zfetch_poly.<locals>.<dictcomp>)Úpolymarket_liver  rÉ   rd  r   )rƒ   r,   r(  rÇ   rÀ  r   r   r   Ú
fetch_polyO  s    rÄ  c                 C   sV   z<ddl }|j|| || ¡ d�}|s*W dS dd„ | ¡ D ƒW S  tyP   Y dS 0 dS )z�Fetch home/draw/away prices from Polymarket US. Returns polyus_-prefixed
    dict, or None. Only called for GAP_LEAGUES (no Poly.com coverage).r   NrÂ  c                 S   s   i | ]\}}d |› �|“qS )r9   r   r\  r   r   r   rY  c  r5   z fetch_polyus.<locals>.<dictcomp>)rô   r  rÉ   rd  r   )rƒ   r,   r(  rÇ   rô   rÀ  r   r   r   Úfetch_polyus[  s    rÅ  c              	      sÂ   ˆ   d¡pt ¡  ¡ }t|ƒ}tdƒD ]–}zt| ¡  }t|dddd��B}tj	|t
d�}|rb| ¡  | ‡ fdd	„t
D ƒ¡ W d   ƒ n1 sŽ0    Y  W  d S  tyº   t d
¡ Y q&0 q&d S )NrÇ   rw  r+  r   r   rx  rz  c                    s   i | ]}|ˆ   |d ¡“qS r¶  r£   r·  r¸  r   r   rY  r  r5   zlog_row.<locals>.<dictcomp>r   )rB   r   rì   r&  r´  r€  rë   r   r‚  rƒ  r½  r„  r…  r†  r9  r‡  )rC   rü   r   rŒ  r�  re   r¨   r   r¸  r   Úlog_rowh  s    
6rÆ  c                 C   s’   |d u rt  ¡ }tdd› �ƒ tdt ¡  d¡› �ƒ |d u rdtdƒ td| › d|› d|› d	�ƒ ntd› ƒ t|| ||||ƒ td› d�ƒ d S )
NÚ
z<============================================================zHALFTIME ALERT  z%H:%M:%Su8   Score unknown (API unavailable) â€” check score manuallyz(Then run: check_halftime(date.today(), 'z', 'z
', H, A, 'z'))r   rì   r)   r   rñ   r¬   r  )rƒ   r,   r(  Zscore_hZscore_arÇ   r   r   r   rú   x  s    
rú   c            7      C   sd  t dt ¡ › d�ƒ t dt› �ƒ t dt› dt td¡› �ƒ tdkrNt dƒ t d	ƒ tƒ  i } d
}d}d
}d
}i }d
}g }t ¡ }t	 
¡ ràzt	 ¡  W n: tyÒ }	 z"t dt	› d|	› d�ƒ W Y d }	~	nd }	~	0 0 t dƒ d S zt ¡ j}
W n   d
}
Y n0 t ¡ �rt ¡  t dƒ |
|k�r¢tƒ \}}dd„ |D ƒ}|D ]´}|d  ¡ |d  ¡ f}|| v�r¼t d|d › d|d › d|d › d|d › �ƒ i |¥ddd d dd d d d dœ	¥| |< n6dD ]}|| | | |< �qÀ|d �r@|d | | d< �q@d}t|  ¡ ƒD ]‚}||v�r| | }|d d u �r`| d ¡|v�r`d|d!< t d"|d › d|d › �ƒ n&|  |¡ t d#|d › d|d › �ƒ �q| �sžt d$t› �ƒ |
}t|  ¡ ƒD ]Z}| | }| d!¡�r®|d �sà| d ¡d%v �r®|  |¡ t d&|d › d|d › �ƒ �q®| �st t¡ q||| tk�r^tƒ }|}|tk �rZ|�rTt d'|› d(�ƒ d}nd}|�r–|| tk�r–d)d*„ |  ¡ D ƒ}|�r–t|ƒ}|}t|  ¡ ƒD �	]J\}}|d �rÔt  !¡ |d   "¡ d+k�rÔ�q¢t#|d | d,t ¡ ¡ƒ}|d u�rt|d-k �rt|d.k�r¢|�r¢|d/ }|d u �s:t  !¡ |  "¡ d0k�r¢|d �s€z&t$|d |d |d |d, ƒ|d< W n t%�y~   Y n0 d1\}}|d �r²t&|d t'|ƒ| d¡d2�\}}| d,t ¡ ¡}t(|d |d |d |ƒ}t)|d |d |d |ƒ}t*|d |d |d |ƒ}t+|ƒ}t  !¡  ,d3¡t-|ƒ|d |d |d d4|d
d
|�rR|d5 nd6|�rb|d7 nd6|�rr|d8 nd6|�r‚|d5 nd6|�r’|d7 nd6|�r¢|d8 nd6d9œ}|�rº| .|¡ |�rÊ| .|¡ |�rÚ| .|¡ t/|ƒ t0|ƒ t  !¡ |d/< d
d:l m1}  t-| d,d6¡ƒt-t ¡ ƒt-t ¡ | d;d<� ƒfv }!d }"| d=¡�sˆ|!�rˆt2|||ƒ}#|#�rˆt3|#ƒ}"d|d=< t4d>k�rˆ|"|#d?< | 5|#¡ t4dk�rÜ| d@¡�sÜ|!�rÜt2|||dddA�}$|$�rÜ|"�pÄi |$d?< | 5|$¡ d|d@< |�sædBn|d8 dC›dD�}%|�s dBn|d8 dC›dD�}&|�r"| dEd¡nd }'|'�s0dBn|'› dD�}(t dF|dG›dH|d › d|d › dI|%› dJ|&› dK|(› �ƒ �q¢dLt  !¡  ,dM¡› dN|d › d|d › �})|dO �s | d¡�r |�r t#|d | d,t ¡ ¡ƒ}|d u�rø|d
k �rød|dO< t6|ƒ nd|dO< d }*|�rè|d �sÐ| dP¡}+|+d u �s>t  !¡ |+  "¡ dQk�rÐt  !¡ |dP< zHt$|d |d |d |d, ƒ|d< |d �r�t dR|)› dS|d › �ƒ W n< t%�yÎ }	 z"t dR|)› dT|	› dU�ƒ W Y d }	~	n
d }	~	0 0 |d �rè| |d ¡}*|*�rX|*dV },|*d �	pd
}-|*d �	pd
}.|*dW }/t  !¡ }0|/|dX k�	r@|/|dX< |0|dY< |/�	r||dY �	r||,dZv�	r||0|dY   "¡ d[ }1t+|/|1 ƒ}2n:|,d\v �	r¬t+t#|d | d,t ¡ ¡ƒ�	p¦d
ƒ}2n
|/�	p´d
}2t dR|)› d]|-› d^|.› d_|,› d`|2› da�ƒ | d,t ¡ ¡}t(|d |d |d |ƒ}t)|d |d |d |ƒ}|d d u�
o6|,dbv }3|3�
sž|2dck�
rž|0 ,d3¡t-|ƒ|d |d |d |,|2|-|.ddœ	}|�
r†| .|¡ |�
r–| .|¡ t0|ƒ |,dek�
rÚ|df �
sÚd|df< t7|d |d |d |-|.|ƒ nr|,dbv �rL|d �sLt  !¡ |d< |df �st dR|)› dg|,› dh�ƒ n0t dR|)› d]|,› di|d j8d
d
dj� ,dM¡› dk�ƒ |,|d < �q¢|d �r¢| d,t ¡ ¡}t#|d |ƒ}4|4d u�r¢|4d
k�r¢|4dck�r¢|d �s¢|�s²dlndm}5|4dnk �rÆdo}6n$|4dpk �rÖde}6n|4dqk �rædr}6nds}6t  !¡ }0t(|d |d |d |ƒ}t)|d |d |d |ƒ}|0 ,d3¡t-|ƒ|d |d |d |6t+|4ƒd d ddœ	}|�rh| .|¡ |�rx| .|¡ t0|ƒ t dR|)› d]|4dt›du|5› dv�ƒ |df �s¢t9|d |ƒ�r¢t dR|)› dw�ƒ d|df< t7|d |d |d d d ƒ �q¢|�r|| dxk�rt:|ƒ |}g }| �rTt;dydz„ |  ¡ D ƒƒ�rTt d{t  !¡  ,dM¡› d|�ƒ |  <¡  t t¡ q|d S )}Nu   
=== Live Match Monitor â€” z ===zWatchlist: zStrategy:  u    â€” rl  r!   zR  NOTE: xlow fires ONLY on matches where BOTH Kalshi and Polymarket quote a price.uD   Edit and save the file any time â€” picks up changes automatically.
r   Tz  [RECYCLE] cannot clear r	  z5) -- staying up rather than exiting into a stuck flagzO  [RECYCLE] requested -- exiting; SoccerMonitorGuard will restart within ~5 minz3  [config] league_map.json / team_map.json reloadedc                 S   s$   h | ]}|d    ¡ |d   ¡ f’qS )rƒ   r,   r[  )r’   Úmr   r   r   r—   µ  r5   zmain.<locals>.<setcomp>rƒ   r,   z  + Watching: r„   rª   z) ko=r«   F)	ÚalertedÚclv_alertedr¢  Úft_timeÚclv_checkedÚlast_api_elapsedÚapi_elapsed_sinceÚlast_prematch_logÚfid_last_tried)r«   rª   rÇ   rÓ   )NÚNSÚTBDr=  r>  r?  rË  r¢  Zfinish_then_dropz   - Removal deferred (in play): z  - Removed: u%     Watchlist empty â€” add matches to )r=  r>  r?  z  - Removed (finished): z  [!] API-Football quota low (u0    left) â€” switching to clock-based HT detectionc                 S   sD   g | ]<}|d  r|d r0t  ¡ |d   ¡ dkst|ƒr|d  ‘qS )rÓ   rË  r<  )r   rñ   rž  r©  ©r’   r¨   r   r   r   r“     s   (þzmain.<locals>.<listcomp>r<  rÇ   iÄÿÿÿiVúÿÿrÏ  i„  r]  ri  rÕ   ZPREr*  r   ré   r+  )rp  rÇ   r6   rƒ   r,   r  r‘  r­  r®  r­   r®   r¯   r�   rq  r‚   r  r   r  rÊ  r}  rý   Zxlow_alerted)rE   r¼   r°   rÚ   ro  r|   z  [PRE z+dzmin] z: Pin=z  B365=z	  Kalshi=ú[rœ  z] rÌ  rÐ  r   ræ   z: fixture ID z: lookup error (rç   r  r‘  rÍ  rÎ  )rÑ  rÒ  r¥  ÚFTÚAETÚPENr   )rÑ  rÒ  rÞ   r�  z  [rÐ   z'])rÕ  rÖ  r×  éx   )	rp  rÇ   r6   rƒ   r,   r  r‘  r­  r®  r¥  rÉ  z: finished (u#   ) without HT data â€” nothing to dou    â€” will stop polling at )ÚsecondZmicrosecondz +30minz	quota lowzfixture not foundrª  r£  r«  és   r¤  rÕ  rk  zmin elapsed (u   ) â€” logged Kalshi onlyu&   : clock HT window â€” triggering alertrM  c                 s   s.   | ]&}|d  o$t  ¡ |d    ¡ dkV  qdS )rË  r<  N)r   rñ   rž  rÓ  r   r   r   r�     s   ÿzmain.<locals>.<genexpr>z
[u6   ] All matches complete â€” waiting for next watchlist.)=r)   r   rì   r–  r<   Ú
STRATEGIESrB   r¿  r9  Ú	RECYCLE_Frë   ÚunlinkÚOSErrorÚstatÚst_mtimerï   ZchangedÚreloadr›  r'  rî   r  rc  r‡  Ú	POLL_SECSÚQUOTA_CHECK_SECSr  ÚAF_LOW_WATERÚAF_POLL_SECSrZ  r”  rd  r   rñ   rž  r   r,  r   rg  r|  rÁ  rÄ  rÅ  r˜  r¬   r»   Úupdater²  rÆ  r  rÆ   r  r  r¶   rŽ  rú   r   r¬  r  r}  Úclear)7Z	watchlistZ
file_mtimeZapi_okZquota_checked_atZaf_last_pollZ	af_scoresZclv_last_sentZ	clv_batchZnow_tsr*   Úmtimer±  ZfreshZ
fresh_keysrÈ  r›   re   Z_NOT_STARTEDr   r¨   Z	remainingZ	live_fidsr‘  ZlastZpre_b365Zpre_pinrü   rØ   ZpolyrÙ   r±   rC   Z_tdZ_gd_okrý   Z	clv_alertZ
xlow_alertr´   Zb365_strZkal_awayZkal_strr  ZscoreZ
last_triedr  r*  r+  Zapi_elapsedZnow_dtZextraZest_elapsedZalready_terminalZclock_elÚreasonZ
est_statusr   r   r   Úmain‡  s*   ,




0þ


 ÿÿ
 


ÿ$ 
&
þ

ñ


ÿ


ÿ
 ÿÿÿ(


 "
,

&
(÷

 
0
(


÷

 þ
rê  Ú__main__zLive match monitor)Zdescriptionz
--strategyz-signal rule to fire on (default: %(default)s))ÚchoicesÚdefaultÚhelp)N)NT)N)N)NNN)N){Ú__doc__Úsysr  r9  r‚  rU   rJ   r   r   ÚstdoutÚreconfigureÚpathlibr   Zemail.mime.multipartr	   Zemail.mime.textr
   rï   ZAPI_KEYrâ  rå  r³  r�  rä  rã  r–  rÜ  r~  r   Z_envrB   rT   r   r   rL   rO   rQ   r=   r>   r?   r@   rµ   r]   r^   rÛ  r<   r  rm   rn   rl   ró   r÷   r(   ra  r+   r%   rG   rN   rP   rR   rX   rÆ   r  r  Zkalshi_liver  r  r  r  rÃ  r  r  r  r  r,  r2  r0  r7  r8  r;  rC  rB  rK  rJ  rL  ZPLAYABLE_BACKSTOP_TTLrb  r^  rP  rQ  rO  rR  rg  rh  rŽ  r”  r›  r   r|  r§  r¨  r©  r¬  r½  r²  r´  r¿  rÁ  rÄ  rÅ  rÆ  rú   rê  Ú__name__ÚargparseÚArgumentParserZ_apÚadd_argumentrî   Zparse_known_argsZ_argsr±  rE   r   r   r   r   Ú<module>   sæ   0
þ
-
 "  iO!	


 
Y (+	
   
ÿ