a
    3°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mZ ddl	m
Z
 ddlmZ ej dd¡ ej dd¡ e
dƒZe
d	ƒZe
d
ƒZdejv Zd\ZZdZdZdZdd„ Zdd„ Zdd„ Zdd„ Zdd„ Zdtdd„Zdd„ Zdd„ Z d d!„ Z!d"d#„ Z"d$d%„ Z#d&d'„ Z$d(d)„ Z%d*d+„ Z&g d,¢Z'd-Z(d.d/hZ)d0d1„ Z*d2d3„ Z+d4Z,d5Z-d6d7„ Z.d8d9„ Z/dud:d;„Z0d<d=„ Z1d>Z2d?Z3d@dA„ Z4dvdCdD„Z5dwdEdF„Z6dxdHdI„Z7dJdK„ Z8dydLdM„Z9dNdO„ Z:dPZ;dQZ<dRZ=dzdSdT„Z>d{dUdV„Z?dWdX„ Z@dYdZ„ ZAd[d\„ ZBeCd]k�räd^ejv �rxe:ƒ ZDejEeDd_d`� e$ƒ da  Fdb¡ZGdce H¡  Idd¡ ZJeG�rXeJdeeGdf › dgeGdh di›dj�7 ZJeAeJeBƒ eDdkƒ edleJ› �ƒ nldmejv �r´ddnlKmLZL eLdce H¡  Idd¡ e@ƒ ƒ edoƒ n0e:ƒ ZMejEeMd_d`� edpe› dqeNeMƒdr›ds�ƒ dS )|a
  
Soccer betting dashboard -- generates a self-contained HTML snapshot of
everything worth watching, from local state + live venue APIs.

Answers the questions that otherwise get asked ad-hoc:
  - what are today's/tomorrow's xlow signals, and did they actually trade?
  - what real positions are open, on which venue, at what cost/value?
  - how is the frozen xlow forward test tracking? (its n>=100 checkpoint was
    passed long ago -- the open question is the review it was meant to trigger)
  - which leagues are actually working end-to-end, and which are blocked?

Usage:  python main\dashboard.py            -> writes E:\soccer\dashboard.html
        python main\dashboard.py --quiet    -> no stdout chatter (for scheduling)

Read-only. Places no orders, mutates no state.
é    N)ÚdateÚ	timedeltaÚdatetimeÚtimezone)ÚPath)ÚdefaultdictzE:\soccer\mainzE:\soccer\researchzE:\soccer\datazE:\soccer\live_datazE:\soccer\dashboard.htmlz--quiet©gš™™™™™©?g¸…ëQ¸¾?ç      $@g     @�@é2   c                  G   s   t st| Ž  d S ©N)ÚQUIETÚprint)Úa© r   úmain/dashboard.pyÚlogF   s    r   c                 C   s4   zt  t| ƒjdd�¡W S  ty.   | Y S 0 d S )Núutf-8©Úencoding)ÚjsonÚloadsr   Ú	read_textÚ	Exception)ÚpÚdefaultr   r   r   Ú_jloadK   s    r   c                  C   sœ  z"ddl } |  d¡ dg ¡pg }W n< ty^ } z$dt|ƒdd… dœgW  Y d}~S d}~0 0 t|ƒdkrpg S dd	„ }g }t|d
d„ d�D �]}| dd¡}||| d¡ƒdddddœ}z–| jdd|id� d¡pÖg }|D ]F}	||	 d¡ƒ}
|
dk�r
|d  d7  < qÜ|
dk rÜ|d  d7  < qÜt| jddd|dœd� d¡�pFg ƒ|d< W n: t�yŠ } z t|ƒdd… |d< W Y d}~n
d}~0 0 | |¡ qŒ|S )as  Cash / positions / resting orders per Kalshi subaccount.

    THE APP CANNOT SHOW THIS. Kalshi's Portfolio screen displays the PRIMARY
    subaccount only -- the arrow beside it toggles deposit/withdraw, it is not
    an account switcher (checked with the user 2026-08-24). So a $5 transfer to
    subaccount 1 read as a -$4.46 portfolio loss in the app, and anything that
    ever runs on a non-primary subaccount would be invisible on the phone.
    This panel is the only place it can be seen.

    Reads are subaccount-scoped by the API, and the DEFAULT for positions and
    orders is subaccount 0 -- byte-identical to passing subaccount=0, which is
    why the rest of this file needed no changes when subaccount 1 was created.

    Returns [] when only the primary exists, so the section renders nothing on
    a single-subaccount account rather than adding an empty box.
    r   Nz/portfolio/subaccounts/balancesZsubaccount_balanceséÈ   )ÚnÚerroré   c              	   S   s(   z
t | ƒW S  ttfy"   Y dS 0 d S )Nç        ©ÚfloatÚ	TypeErrorÚ
ValueError©Úxr   r   r   Ú_nump   s    
zsubaccounts.<locals>._numc                 S   s   |   dd¡S )NÚsubaccount_numberr   ©Úgetr%   r   r   r   Ú<lambda>w   ó    zsubaccounts.<locals>.<lambda>©Úkeyr(   Úbalance)r   ÚcashÚlongÚshortÚrestingr   ú/portfolio/positionsÚ
subaccount©ÚparamsÚmarket_positionsÚposition_fpr1   r2   z/portfolio/ordersr3   )ÚstatusÚlimitr5   Úorderséx   r   )Úkalshi_traderÚ_getr*   r   ÚstrÚlenÚsortedÚappend)ÚktZbalsÚer'   ÚoutÚbr   ÚrowZmpr   Úqr   r   r   ÚsubaccountsT   sX    
ÿÿ.ÿÿÿ
ÿÿþý*rJ   c                  C   sØ  d g d dœd g d dœdœ} �zþdd l }i i  }}dD ]n}zR|jdd|id�pNi }t| dd¡p`dƒd	 ||< t| d
d¡p|dƒd	 ||< W q4 ty    Y q4Y q40 q4|| d d< t| ¡ ƒ| d d< t| ¡ ƒ| d d< | d d | d d  | d d< t| ¡  dg ¡�pg ƒ}dd„ |D ƒ}dD ]`}zD|jdd|id� d¡�pFg D ] }| d¡|v�rH| 	|¡ �qHW n t�y€   Y n0 �q$|D ]Œ}t| d¡�pždƒdk�r¬�qŠ|d }	| d d  	|	|	 
dd¡d |	 
dd¡d | d¡t| d¡�pödƒd t| d¡�p
dƒdœ¡ �qŠW n> t�yX }
 z$t|
ƒd d… | d d< W Y d }
~
n
d }
~
0 0 �z8dd l}| ¡ d d }t|d  ƒ| d! d< d"}| ¡  d¡�p i  ¡ D ]Æ\}}| d#i ¡�p¾i }| d$¡�pÎi  d%d&¡}| d'|¡}| d! d  	|› d(|› � d)¡||�p|| d*¡t|d+ d, ƒt|d- d, ƒt| d.¡�p@i  d,¡�pLdƒdœ¡ |t|d- d, ƒ7 }�q¦|| d! d< | d! d | | d! d< W n> t�yÒ }
 z$t|
ƒd d… | d! d< W Y d }
~
n
d }
~
0 0 | S )/N)r/   Ú	positionsr   ©ÚkalshiÚpolyusr   )r   r   z/portfolio/balancer5   r6   r/   ç      Y@Zportfolio_valuerM   Úcash_by_subr0   Úpendingr8   c                 S   s   h | ]}|  d ¡’qS ©Útickerr)   )Ú.0Úmr   r   r   Ú	<setcomp>²   r,   zvenue_state.<locals>.<setcomp>)r   r4   rS   r9   rK   ú-r   éÿÿÿÿZmarket_exposure_dollarsZfees_paid_dollars)ÚrefÚgameÚlegÚqtyÚcostÚvalueÚfeesr   r   ZbalancesZcurrentBalancerN   r    ZmarketMetadataÚteamÚnameÚ Útitleu    â€” u    â€”ZnetPositionDecimalr]   r^   Z	cashValuer_   )r>   r?   r"   r*   r   ÚsumÚvaluesÚlistÚget_positionsrC   Úrsplitr@   Zpoly_us_authZget_account_balancesÚitemsÚstrip)rF   rD   Z_cashZ_pendingZ_subZ_bZ_mpZ_seen0rU   ZtkrrE   ZparG   Z_pus_pendingZslugr   Zmdr`   rc   r   r   r   Úvenue_stateŽ   sŽ    

ÿ
  ÿþþ
ù	.
 ù	.rk   c               	   C   s    t jd d } h d£}i }zjt| ddd��H}t |¡D ],}| d¡r6| d¡|v r6|d ||d < q6W d	  ƒ n1 sx0    Y  W n ttfyš   Y n0 |S )
a  event -> pricing arm, from the maker's append-only assignment log.

    Only arms the maker still recognises are returned: the retired vol/ko
    SELECTION experiment wrote into the same log, and a stale label rendered as
    a current one would be worse than no label.
    Zresearchzmm_arm_log.csv>   ZbudgetZtieloadZlatch3rb   r   ©Únewliner   ÚeventZarmN)ÚDATAÚparentÚopenÚcsvÚ
DictReaderr*   ÚOSErrorr$   )ÚpathÚliverF   ÚfhÚrr   r   r   Ú
arm_labelsó   s    4ry   c                 C   sÌ   t td g ƒ}t td g ƒ}g }|D ],}| d¡| vr8q$| t|t||ƒd�¡ q$|D ]`}| d¡| vrjqV| d¡}|d u r†| d¡}|d u r˜| d¡}| t|| d¡|d	œd�¡ qV|jd
d„ d� |S )Nzpending_bets.jsonzbet_history.jsonr   )ÚsettledZ
kalshi_pnlZpoly_pnlÚpnlÚftr)r|   r{   c                 S   s   | d | d | d fS )Nr   ÚdivÚmatchr   )rx   r   r   r   r+     r,   zsignals_for.<locals>.<lambda>r-   )r   ro   r*   rC   Ú_sig_rowÚ_provisionalÚsort)ZdatesÚftr_mapZpendZhistÚrowsrG   r{   r   r   r   Úsignals_for  s$    


 r„   c                    s¶   t | d¡pg ƒ‰ˆs| dfS z(ddlm}m} ˆt |ƒt |ƒ O ‰W n tyX   Y n0 zddlm‰  W n ty€   d‰ Y n0 ‡ ‡fdd„‰‡fdd	„| D ƒ}|t| ƒt|ƒ fS )
ab  Signals in the live scope, plus how many were dropped.

    The signals panel is an OPERATIONAL view -- what the rule is doing now --
    so leagues outside the scope do not belong in it (user, 2026-08-23). They
    are not lost: they keep firing into pending_bets and the forward test, and
    the by-league table still carries them (the "outside scope, tracking"
    summary row was removed from the page 2026-09-12; the series survives in
    forward_test_unique_book_log.csv).

    Returns (kept, hidden_count) rather than filtering silently -- a table that
    quietly drops rows is how you end up trusting a number that is missing its
    denominator. The count is rendered next to the heading.

    Falls back to NO filtering if the scope is unavailable (forward_test error),
    because showing everything is a much smaller failure than showing nothing.
    Ú
bt_leaguesr   ©ÚAUTO_TRADE_LEAGUESÚAUTO_TRADE_EXCLUDE_LEAGUES©ÚXLOW_PRICE_FLOORgš™™™™™¹?c                    s(   | d ˆvrdS t | ƒ}|d u p&|ˆ kS )Nr}   F)Ú_row_ask©rx   r   )Ú_floorÚscoper   r   Ú_in_viewZ  s    zscope_signals.<locals>._in_viewc                    s   g | ]}ˆ |ƒr|‘qS r   r   ©rT   rx   )r�   r   r   Ú
<listcomp>a  r,   z!scope_signals.<locals>.<listcomp>)	Úsetr*   Údaily_signalsr‡   rˆ   r   Úforward_test_unique_bookrŠ   rA   )rƒ   Úftr‡   rˆ   Zkeptr   )r�   r�   rŽ   r   Úscope_signals   s    
r–   c                 C   sP   |   d¡dkr|   d¡n|   d¡}|du rL|   d¡durB|   d¡n|   d¡}|S )zÒThe price a signal fired at, on the venue it fired on.

    Mirrors the Ask column's own resolution in the signals table exactly, so
    the filter above can never disagree with the number the page prints.
    ÚcheapÚKalshiÚask_kÚask_pNr)   rŒ   r   r   r   r‹   e  s    ""r‹   c           	      C   s¬   |sdS |   d¡|   d¡ }}|   d¡|   d¡ }}|  ||||f¡pT|  |||f¡}|s^dS |   d¡pjd ¡ }d|v r‚|d	k}nd
|v r”|dk}ndS ||r¢dndddœS )a'  A pending bet's result read off the match record, or None.

    Same shape as the settled path, plus provisional=True. `pnl` carries only a
    SIGN: the table scores at PAPER_NOTIONAL off the ask exactly as it does for
    reconciled bets, so the two paths cannot disagree about the money.
    Nr   r}   ÚhomeÚawayÚbetrb   ÚHOMEÚHÚAWAYÚAç      ð?ç      ð¿T)r|   r{   Úprovisional)r*   Úupper)	rG   r‚   ÚgdÚlgÚhmÚawr|   r�   Úwonr   r   r   r€   q  s    "

r€   c           	   	   C   sð   z0t |ƒ}d|  k rdk r.n nt|dƒW S W n ttfyF   Y n0 zHt | ƒ}t |ƒd }|d|  }d|  k r~dk rŽn nt|dƒW S W n ttfy¦   Y n0 z*t |ƒ}d|  k rÆdk rÎn n|ndW S  ttfyê   Y dS 0 dS )ae  The price the SIGNAL fired at, for the paper/research view.

    The rule evaluates CLV on the MID. `*_ask` is the quoted ask at that
    instant, and on a normal book it sits a cent away. On an empty book it does
    not: FC Sion v FC Thun had a Poly-US mid near 0.50 (giving clv_polyus
    +7.8%) and an ask of 0.99, so the row rendered "0.99 (P) +7.8%" -- a price
    and an edge that describe different prices, and a paper result scored at
    0.99 instead of at the price the rule actually saw.

    Order of preference:
      1. the mid recorded at signal time (entry_k_mid / entry_p_mid)
      2. the price implied by fair and the stored CLV -- fair*(1-clv), which is
         the mid by definition, reconstructed when the mid was not written
      3. the stored ask, unchanged

    DISPLAY ONLY. The ledger is untouched and the forward test never reads it.
    r   r   é   rO   r¢   N)r"   Úroundr#   r$   )	ÚfairZclv_pctZrecorded_midZ
stored_askrU   ÚfÚcÚpxr   r   r   r   Ú
_signal_px‰  s$    "r±   c                 C   s&  g }|   d¡r| d¡ |   d¡r,| d¡ d }|   d¡rJ|   d¡sJd}n|   d¡rXd}dd	„ }|   d
d¡|   dd¡|   dd¡› d|   dd¡› �|   d¡|   d¡|   dd¡|   d¡p¶d||   d¡|   d¡t|   d¡|   d¡|   d¡|   d¡ƒt|   d¡|   d¡|   d¡|   d¡ƒ||   d¡ƒ||dœS )NZauto_order_idr˜   Zauto_polyus_order_idúPoly-USZkalshi_stakeZ
poly_stakeÚPolyc              	   S   s(   z
t | ƒW S  ttfy"   Y d S 0 d S r   r!   r%   r   r   r   Ú_fº  s    
z_sig_row.<locals>._fr   rb   r}   ú?r›   z vs rœ   r�   ÚstrategyZbaselineZ
clv_kalshiZclv_polyZpin_fairZentry_k_midZ
kalshi_askZentry_p_midZpoly_ask)r   r}   r~   r›   rœ   r�   r¶   r—   Úclv_kÚclv_pr™   rš   r­   Útradedrz   )r*   rC   r±   )rG   rz   Úvenuer—   r´   r   r   r   r   ¯  s>    







ÿÿçr   c            #         s  �zªdd l ‰ ˆ jdd�‰dd„ ˆD ƒ} ‡ fdd„| D ƒ}ˆ  |¡}ˆ  |¡}|ˆ  ‡ fdd„|D ƒ¡ˆ  ˆ  |¡¡dœ}ˆ  |¡\}}ˆ  |¡|d	< ˆ  |¡|d
< ttƒ}|D ]$}ˆ  |¡}	|	r®||d   	|	¡ q®‡ fdd„t
| ¡ ‡ fdd„d�D ƒ}
zZˆ  |¡\}}ˆ  |¡ˆ  |¡|ˆ  |¡ˆ  ˆ  | ¡¡ˆ  ˆ  | ¡¡ˆ jˆ jˆ jdœ	}W n t�yl   d }Y n0 ttƒ}|D ](}ˆ  |¡}	|	�rz||d   	|	¡ �qz‡ fdd„t
| ¡ dd„ d�D ƒ}ˆ  dd„ | ¡ D ƒ¡}|�rì||d< t ¡  ¡ t ¡ tdd�  ¡ h}i }zFt
|ƒD ]8‰ˆ  ‡fdd„|D ƒ¡}t
dd„ | ¡ D ƒƒ|ˆ< �qW n t�yp   i }Y n0 t ¡ ‰ˆtdd� ‰‡ ‡‡‡fdd„}d|ˆ jˆ jƒi}d d!„ }d }z ˆ  |¡\}}d"d#„ |D ƒ}W n t�yð   d }Y n0 g g  }}d$ }}|D ]t}	|	d% �r*d|	d&  |	d&  nd'}||7 }| 	|	d( |f¡ |d u�rt|	ƒ|v �rh||7 }| 	|	d( |f¡ �qi }i }ˆD ]â}|d( |v�sŠ| d)¡�s¬�qŠ|d* �rÂ|d* d ni } |d) ||d( |d |  d+¡|  d,¡f< |d) ||d( |d |  d+¡f< | d-¡�p"| d.¡}!|!�rŠ|!||d( |d |  d+¡|  d,¡f< |!||d( |d |  d+¡f< �qŠ||
|||||�rˆ|d/ nd|t
ˆ jƒ||||ˆ j ˆ jd d0œW S  t�y }" zNi i d i i d di i g dd g g ddd1œig g d2d3t!|"ƒd d4… d5œW  Y d }"~"S d }"~"0 0 d S )6Nr   F)Zrequire_resultc                 S   s   g | ]}|d  dur|‘qS )r|   Nr   ©rT   Úfxr   r   r   r‘   ç  r,   z forward_test.<locals>.<listcomp>c                    s   g | ]}|d  ˆ j kr|‘qS ©r¦   )ÚFREEZE_DATEr»   ©ÚFr   r   r‘   è  r,   c                    s   g | ]}|d  ˆ j kr|‘qS )r°   r‰   ©rT   rG   r¿   r   r   r‘      s   ÿ)õ   xlow (PRIMARY Â· scoped)Zxlow_f10Zt60ÚuniqueÚoverlapÚleaguec                    s   i | ]\}}|ˆ   |¡“qS r   ©Ústat©rT   r§   Úbsr¿   r   r   Ú
<dictcomp>  r,   z forward_test.<locals>.<dictcomp>c                    s   | d ˆ j vt| d ƒ fS )Nr   r   )ÚPRIMARY_LEAGUESrA   ©Zkvr¿   r   r   r+     s   ÿzforward_test.<locals>.<lambda>r-   )	ÚstateÚdisc_onÚdisc_offÚ	disc_dropÚpost_onÚpost_offÚfreezeÚmin_nÚfloorc                    s   i | ]\}}|ˆ   |¡“qS r   rÆ   rÈ   r¿   r   r   rÊ   8  r,   c                 S   s   t | d ƒ S ©Nr   ©rA   rÌ   r   r   r   r+   9  r,   c                 S   s   g | ]}|D ]}|‘qqS r   r   ©rT   rÉ   rG   r   r   r   r‘   :  r,   zxlow_polyus (USL/PL1/CH1)r   ©Údaysc                    s   g | ]}|d  ˆ k r|‘qS r½   r   rÁ   )Ú_dr   r   r‘   W  r,   c                 s   s   | ]\}}|d  r|V  qdS )ÚdroppedNr   ©rT   r§   Úsr   r   r   Ú	<genexpr>X  s   zforward_test.<locals>.<genexpr>é   )Zhoursc                    sÌ   g g  }}ˆD ]d}|  d¡}|rˆ|  kr4ˆks8q q|rJ|d |krJq| |ƒ}|sXq|d d urh|n| |¡ q|j‡fdd„d� |j‡fdd„d� tdd	„ |D ƒƒ}ˆ  |¡|||t|ƒ| d
œS )NÚkor¦   r|   c                    s   |   d¡pˆ S ©Nrá   r)   ©rG   ©Úcutoffr   r   r+   �  r,   z/forward_test.<locals>._last24.<locals>.<lambda>r-   c                    s   |   d¡pˆ S râ   r)   rã   rä   r   r   r+   ‚  r,   c                 s   s   | ]}|d  rdV  qdS )rª   r   Nr   rÁ   r   r   r   rß   ƒ  r,   z0forward_test.<locals>._last24.<locals>.<genexpr>©rÇ   Úbetsrq   ÚwÚl)r*   rC   r�   rd   rÇ   rA   )ÚfirerÓ   rz   Zopen_r¼   rá   rG   Zwins)rÀ   Úallfxrå   Ú	now_localr   r   Ú_last24q  s"    

ÿzforward_test.<locals>._last24Úxlowc                 S   s\   dg  }}| D ]H}||ƒ}|s q||d r>d|d  |d  nd7 }|  |d |f¡ q|S )Nr    rª   r   r°   r£   r¦   ©rC   )Zfixturesrê   ZcumrF   r¼   rG   r   r   r   Ú_curveŽ  s    
$zforward_test.<locals>._curvec                 S   s   h | ]}t |ƒ’qS r   )ÚidrÁ   r   r   r   rV   ¦  r,   zforward_test.<locals>.<setcomp>r    rª   r°   r£   r¦   rá   Zprer›   rœ   Zftr_displayr|   r   )rƒ   ÚleaguesÚzgateÚzgate_drop_by_dateÚgap_leaguesÚgap_allr   Úlast24r…   Úko_mapr‚   Ú
xlow_curveÚxlow_curve_zgateÚbandrÓ   r   ræ   r   rµ   i,  )rƒ   rò   ró   rô   rõ   rö   r   rø   r‚   r…   r÷   rù   rú   rû   rÓ   r   )"r”   Zload_fixturesZclassify_xlow_btrÇ   Zclassify_t60Zclassifyr   rf   Zfire_xlow_primaryrC   rB   ri   Z
zgate_walkZzgate_stateZclassify_xlow_zgateZclassify_xlow_postzZZGATE_FREEZEZZGATE_MIN_NZZGATE_FLOORr   Zfire_xlow_polyusre   r   ÚtodayÚ	isoformatr   r   Únowr¾   rñ   r*   rË   ZBANDr@   )#ZpreppedZoosZxlow_btÚprimaryrƒ   ZuniqrÄ   Zby_lgr¼   rG   rò   Z_zkeptÚ_zdropró   Úgaprõ   rö   Z	live_daysrô   Z_strí   r÷   rð   Z_kÚ_rù   rú   Z_cumZ_cumzZ_pnlrø   r‚   Zr0Z_rrE   r   )rÀ   rÛ   rë   rå   rì   r   Úforward_testÞ  sì    

ý


ÿÿ
÷




ÿ
ÿÿ


"
( $ õør  c            !         sB  ddl } ddddi ddddœ‰z ddl}|jˆd< |jˆd< W n tyP   Y n0 �z®tdƒ}| ¡ slˆW S ttƒ}tt	ƒ}t
ƒ }t|d	d
��ª}|  |¡D ]Ž}| d¡pªd dd¡d }|s¾qš| |¡ t| d¡ƒ ¡ dv rš||  d7  < z$||  t	| d¡�pdƒ7  < W qš t�y&   Y qš0 qšW d  ƒ n1 �s@0    Y  t|ƒˆd< tdd„ ƒ}	|D ]B}| |d¡}
|	|
 d  d7  < |	|
 d  | |d¡7  < �qfdd„ t|	 ¡ ƒD ƒˆd< |	d d ˆd< |	d d |	d d  ˆd< t| ¡ ƒˆd< i }t|d	d
���Ž}|  |¡D �]p}| d¡�p2d dd¡d }|�sJ�q| || dd¡| dd¡| d¡�prddd … | d!d¡g g g d"œ¡}| d#¡}|d$v�rÒz|d%  t	|ƒ¡ W n t�yÐ   Y n0 t| d¡ƒ ¡ dv �rLzt	| d¡�püdƒ}W n t�y   d}Y n0 |d&  | d'd(¡| d)d¡| d*d¡|d+œ¡ z,t	| d)¡�p^dƒ}|�rv|d,  |¡ W n t�yŒ   Y n0 �qW d  ƒ n1 �s¨0    Y  ttƒ}| ¡ D ]¨\}}| |d¡}
||
  ||d |d |d |d! |
t|d, ƒdk�r t|d, ƒd d- ndd. d/d0„ |d& D ƒ¡�p>d1|d% �rTt|d% ƒnd| |d¡d2œ
¡ �qÂ|D ]}
||
 jd3d„ d4� �qpt|ƒˆd5< td6d„ ƒ}t|d	d
��Ê}|  |¡D ]®}t| d¡ƒ ¡ dv�rÜ�q¼| d!¡�pêd(}|| d&  d7  < || d  | d¡�pd dd¡d ¡ z(|| d  t	| d¡�pHdƒ7  < W n t�yf   Y n0 �q¼W d  ƒ n1 �s‚0    Y  d7d„ t| ¡ ƒD ƒˆd8< d9d:„ | ¡ D ƒ}g }d;D ]~‰ ‡ fd<d:„|D ƒ}t|ƒd=k �rè�qÀtd>d0„ |D ƒƒ}td?d0„ |D ƒƒ}| ˆ t|ƒ|d@| t|ƒ ||t|ƒ dAœ¡ �qÀ|ˆdB< dCd:„ | ¡ D ƒ}dDd:„ |D ƒ}dEd:„ |D ƒ}t|ƒˆdF< tdGd0„ | ¡ D ƒƒt|ƒ ˆdH< dIdJ„ }||ƒ||ƒ ˆdK< ˆdL< ˆdK �r ˆdL �r ˆdK dM ˆdL dM k�r ˆdK dM ˆdL dM  }d@ˆdL dM  |  ˆdN< t|ƒt|ƒ }t|ƒ| }|ˆdO< d@| ˆdP< |ˆdK dM  d| ˆdL dM   ˆdQ< ˆ d¡du�r ‡fdRd:„|D ƒ}t|ƒd=k�r tdSd0„ |D ƒƒt|ƒ }t|ƒd@| |ˆdK dM  d| ˆdL dM   dTœˆdU< W n: t�y< }  z t| ƒddV… ˆdW< W Y d} ~ n
d} ~ 0 0 ˆS )Xa¿  Paper market-maker health, framed around the thing that decides P&L.

    The headline is NOT total P&L but COMPLETION RATE. Measured over 242 books:
    a 3-of-3 book runs +1.38c while a 2-of-3 runs -7.62c, because a complete
    book earns the overround outcome-independently and a partial one is a naked
    position. So the dashboard reports how often books complete, split by
    whether they cleared the liquidity gate that predicts it.
    r   Nr    F)ÚbooksÚcompleteÚpartialÚpnl_cÚby_legsZrunningÚgater   r	  Úlive_regimez'E:\soccer\research\mm_paper_results.csvr   r   Z
leg_tickerrb   rW   r   Úfilled)ÚtrueÚ1ZyesZ	pnl_centsr  c                   S   s   ddgS )Nr   r    r   r   r   r   r   r+     r,   zmaker_state.<locals>.<lambda>c                 S   s$   i | ]\}}||d  |d dœ“qS )r   r   )r  r  r   )rT   r   Úvr   r   r   rÊ     r,   zmaker_state.<locals>.<dictcomp>r  é   r  é   r  r  r}   r~   Útsé
   Úregime)r}   r~   r  r  ÚlegsÚasksÚvolsZvolume©Nrb   r  r  Úsiderµ   Zour_askZfill_px)r  ZaskZfillr  r  éd   ú,c                 s   s   | ]}|d  V  qdS )r  Nr   ©rT   r&   r   r   r   rß   @  r,   zmaker_state.<locals>.<genexpr>õ   â€”)
Úevr}   r~   r   r  r   Úgross_cÚsidesÚvol_minr  c                 S   s   | d S )Nr  r   r%   r   r   r   r+   H  r,   r-   Údetailc                   S   s   ddt ƒ dœS )Nr   r    )r  r  r  )r’   r   r   r   r   r+   M  r,   c                 S   s.   i | ]&\}}||d  t |d ƒ|d dœ“qS )r  r  r  )r  r  r  r×   )rT   Úgr  r   r   r   rÊ   Y  s   þÿÚregimesc                 S   s&   g | ]}|D ]}|d  dur|‘qqS ©r   Nr   rØ   r   r   r   r‘   d  r,   zmaker_state.<locals>.<listcomp>)r   r  éú   iô  éè  iÄ	  c                    s   g | ]}|d  ˆ kr|‘qS )r   r   rÁ   )ÚTr   r   r‘   g  r,   é   c                 s   s   | ]}|d  dkrdV  qdS ©r   r  r   Nr   rÁ   r   r   r   rß   j  r,   c                 s   s   | ]}|d  V  qdS )r  Nr   rÁ   r   r   r   rß   k  r,   rO   )Útr  r  Úrater  ÚperÚsweepc                 S   s&   g | ]}|D ]}|d  dur|‘qqS r$  r   rØ   r   r   r   r‘   ~  r,   c                 S   s    g | ]}|d  dkr|d ‘qS )r   r  r  r   rÁ   r   r   r   r‘     r,   c                 S   s0   g | ](}d |d   k r dk rn q|d ‘qS )r   r   r  r  r   rÁ   r   r   r   r‘   €  r,   Úscoredc                 s   s   | ]}t |ƒV  qd S r   r×   )rT   rÉ   r   r   r   rß   ‚  r,   Úunscoredc                    s^   | sd S t | ƒt| ƒ ‰ t| ƒdkrJt ‡ fdd„| D ƒƒt| ƒd  d nd}t| ƒˆ |dœS )Nr   c                 3   s   | ]}|ˆ  d  V  qdS )r  Nr   r  ©rU   r   r   rß   ˆ  r,   z+maker_state.<locals>._ms.<locals>.<genexpr>g      à?r    )r   ÚmeanÚsd)rd   rA   )Zxsr2  r   r0  r   Ú_ms„  s    
ÿ(ÿzmaker_state.<locals>._msÚcompÚpartr1  Ú	breakevenÚriskedÚ	live_rateÚev_cc                    s,   g | ]$}|d  dkr|d ˆ d kr|‘qS )r   r   r   r	  r   rÁ   )rF   r   r   r‘   ž  r,   c                 s   s   | ]}|d  dkrdV  qdS r)  r   rÁ   r   r   r   rß      r,   )r   r+  r9  Úgatedr   r   )rr   Z	mm_quoterZMIN_LEG_VOLUMEZREGIMEr   r   Úexistsr   Úintr"   r’   rq   rs   r*   rh   Úaddr@   Úlowerr$   rA   rB   ri   rd   re   Ú
setdefaultrC   rf   ÚjoinÚminr�   Údict)!Ú_csvÚQr   r  r{   Zeventsrw   rx   r  Zbyr   ÚmetarU   Úrawr¯   r   r!  Zregr"  Zflatr-  rÞ   ZdoneÚtotZallbZcomp_pZpart_pr3  Zspreadr7  ZgbZgprE   r   )r'  rF   r   Úmaker_stateâ  s   	ÿ

$( 
ý




ý*(
÷*(*
þ

þ".(ÿþ*rH  c            
      C   s@  t tdƒi ƒ} | sdS |  d¡p"g }d}z.t t| d ƒ¡}t |j¡|  ¡ d }W n t	yh   Y n0 |  d¡pvi }t
dd„ | ¡ D ƒƒ}t
d	d„ | ¡ D ƒƒ}|r´t|ƒd
 nd}| |¡pÄi }|  d¡pÒg }	|  d¡|  d¡|  d¡|||	|  d¡t
dd„ |D ƒƒt|ƒ|||t| d¡�p&g ƒt| d¡�p8g ƒdœS )a%  What the liquidity gate is doing RIGHT NOW, from mm_quoter's snapshot.

    The gate rejects with a bare `continue`, so without this the only visible
    evidence was the books that got through. This shows the ones turned away and
    by how much, which is the cost side of the threshold.
    z%E:\soccer\research\mm_gate_state.jsonNrþ   r  ç      N@Údailyc                 s   s    | ]}t | d ¡pg ƒV  qdS )ÚadmittedN©rA   r*   ©rT   r  r   r   r   rß   ¼  r,   zgate_state.<locals>.<genexpr>c                 s   s    | ]}t | d ¡pg ƒV  qdS )ÚrejectedNrL  rM  r   r   r   rß   ½  r,   rX   Úaheadr	  r  Úwindow_hÚ
sweep_secsc                 s   s   | ]}|  d ¡rdV  qdS )Úpassr   Nr)   r�   r   r   r   rß   Ç  r,   rK  rN  )r	  r  rP  rƒ   Úage_mrO  rQ  Únow_passrÚ   ÚadmÚrejrü   Ú	today_admÚ	today_rej)r   r   r*   r   Úfromisoformatr@   rþ   ZtzinfoÚtotal_secondsr   rd   re   rB   rA   )
r"  rƒ   rS  r  rJ  rU  rV  rü   r*  rO  r   r   r   Ú
gate_stateª  s2    

ùr[  ))ú&E:/soccer/research/mm_book_dataset.csvzbook dataset (analysis-ready)at  one row per BOOK we quoted, with the market as it looked AT OPEN -- opening gross, hours to kickoff, book and thinnest-leg volume, worst-leg queue, favourite and longshot price -- and legs_filled / complete as the outcome. Completion is the number the strategy lives or dies on, and a model fitted on legs answers a different question. Built by research/mm_book_dataset.py)z#E:\soccer\research\mm_live_legs.csvzlegs offeredz²one row per leg put on the exchange: league, bid/ask/mid and Pinnacle fair AT PLACEMENT, leg and book volume, queue depth, cents over the touch and the book budget at that moment)ú(E:\soccer\research\mm_live_realfills.csvÚfillszOevery real maker fill, with the queue that was ahead of us and how long it tookg      @zmm_fill_dataset.csvzmm_book_dataset.csvc                     s0   ddl ‰ ‡ fdd„} | ddƒ | ddd	d
� dS )aD  Rebuild BOTH analysis CSVs so the downloads are never stale.

    The book dataset was added 2026-08-26 and NOTHING rebuilt it, so it shipped
    frozen at the row count it happened to have when first generated, while the
    leg dataset beside it refreshed every 30 minutes. A download that silently
    stops moving is worse than no download -- it looks current.

    ORDER MATTERS. The book dataset is derived from the leg dataset, so the leg
    one is rebuilt first and the book builder is told not to redo it
    (rebuild=False); otherwise ~350k log rows get parsed twice per page build
    for the same answer.

    Wrapped per step: these are convenience artifacts and a failure here must
    never take the dashboard down. If one cannot rebuild, the previous file is
    still embedded and simply shows an older row count.
    r   Nc              
      s„   z:ˆ j  | |¡}ˆ j  |¡}|j |¡ |jf i |¤Ž W nD ty~ } z,td| › dt|ƒj	› d|› �ƒ W Y d }~n
d }~0 0 d S )Nz	  [dash] z rebuild skipped: ú: )
ÚutilÚspec_from_file_locationÚmodule_from_specÚloaderÚexec_moduleÚmainr   r   ÚtypeÚ__name__)ra   ru   ÚkwÚspecÚmodrE   ©Ú	importlibr   r   Ú_run  s    z#_refresh_fill_dataset.<locals>._runZmm_fill_datasetz%E:/soccer/research/mm_fill_dataset.pyZmm_book_datasetú%E:/soccer/research/mm_book_dataset.pyF)Zrebuild)Úimportlib.util)rm  r   rk  r   Ú_refresh_fill_datasetý  s    	
ÿrp  c                     sì  dd l } tƒ  dg}g }tD �]D\}}}t|ƒ}z| ¡ }W n tyR   Y qY n0 td| d¡d ƒ}dd l}	|j	d }
}t
|ƒdkr®|j	tvr®|	 |d¡}|j	d d	 }
}t
|ƒd
 }|tkrì| dt|ƒ› dt|ƒ› d|d›d�¡ q|  |¡ d¡}| dt|
ƒ› dt|ƒ› d|› d|› dt|ƒ› d|d›dt
|ƒd d›d�|
 d¡�rVdnd d ¡ q|�sndS | d |¡¡ | d¡ z4dd l‰‡fd d!„}|d"d#ƒ}d$|j|jd%fg}W nR t�y } z8| d&tt|ƒd d'… ƒ› d�¡ d |¡W  Y d }~S d }~0 0 |D ]Ì\}
‰ }}‡ fd(d)„|D ƒ}| d*t|
ƒ› d+t
ˆ ƒ› d,�¡ | d-|› d�¡ | d.¡ | d/¡ ˆ D ]*\}}| d0t|ƒ› d1t|ƒ› d2�¡ �q|| d3¡ |�rÔ| d4td5 |¡ƒ d ¡ | d6¡ �qd |¡S )7Nr   z%<div class="note">download raw data: ó   
r   ztext/csvi@ é   z.gzzapplication/gzipg    €„.Az<span class="dim" title="ú">ú (ú.1fu!   MB â€” too large to embed)</span>Úasciiz<a download="z	" title="z" href="data:z;base64,z</a> <span class="dim">(r  z rows, r&  ú.0fZKBz&nbsp;gzrb   ú)</span>u    &nbsp;Â·&nbsp; ú</div>c                    s*   ˆ j  | |¡}ˆ j  |¡}|j |¡ |S r   )r`  ra  rb  rc  rd  )ra   ru   ZsprU   rk  r   r   Ú_loadK  s    zcsv_downloads.<locals>._loadÚ_mmbdrn  zbook datasetaº  One row = ONE BOOK we quoted, with every feature taken AT OPEN &mdash; what could have been known when the decision to open it was made. Anything measured later is downstream of the outcome, because a book that fills a leg gets re-priced by the budget rule. Outcome is <code>complete</code>. <b>Filter on <code>legs_quoted==3</code></b>: a book where only two legs were ever offered cannot complete, and leaving those in understates the rate.z3<div class="note dim">column glossary unavailable: r=   c                    s   g | ]}|t ˆ ƒvr|‘qS r   )rB  ©rT   r¯   )Údocs_r   r   r‘   d  r,   z!csv_downloads.<locals>.<listcomp>z<details class="sec"><summary>z. &mdash; column glossary <span class="count">(z fields)</span></summary>ú<div class="note">ú<div class="card"><table>z(<tr><th>column</th><th>meaning</th></tr>z<tr><td><code>z</code></td><td class="dim">ú
</td></tr>ú</table></div>z,<div class="note bad">undocumented columns: ú, ú
</details>)Úbase64rp  ÚCSV_DOWNLOADSr   Ú
read_bytesrt   ÚmaxÚcountÚgzipra   rA   Ú
NEVER_GZIPÚcompressÚ
CSV_MAX_MBrC   ÚescZ	b64encodeÚdecodeÚendswithr@  ro  Z
FIELD_DOCSZOUT_COLSr   r@   )r„  rF   Úbitsru   ÚlabelZwhyr   rF  rƒ   r‰  ra   ZmimeZmbZb64rz  r{  ZdatasetsrE   Zcols_ZintroZundocÚcolr   )r}  rl  r   Úcsv_downloads  sš    
ÿ
ÿÿÿþþýýÿ


ÿ

ÿ
 ÿ


ÿ
ÿÿr“  z
2026-09-01r\  c                     s(  ddl ‰ ddl‰‡ ‡fdd„} ddg ddddi ddg ddddœ}t tj¡}�zˆtdƒ}| ¡ �rÚtƒ }| |ƒD �]Z}t	| 
d¡ƒd	krŠqpd}zt |d
 ¡}W n ty²   Y n0 | 
d¡}|dk�r4| | 
d¡¡ |�rÌtd||  ¡ d ƒ}|d du �s||d k �r||d< |dk�rÌ|d  d7  < qp|dv rp|rp||  ¡ dkrp| 
d¡�p^d}	d|	v �rpd}
n@d|	v �sŽdt	| 
d¡ƒv �r”d}
n|	 d¡d dd… �p®|}
|d  
|
d¡d |d |
< qpt|ƒ|d< td ƒ}| ¡ �rt| |ƒƒ}|d!d… |d"< td#d$„ |D ƒƒ|d%< z ttdd&d'd(��$}d)d*„ ˆ  |¡D ƒ}W d  ƒ n1 �s\0    Y  d+d*„ |D ƒ}d,d*„ |D ƒ}t|ƒ|d-< t|ƒ|d.< |�r´d/t|ƒ t|ƒ nd|d0< W n t�yÒ   Y n0 W n> t�y } z$t|ƒj› d1|› �|d2< W Y d}~n
d}~0 0 zÄddl}t| ¡ �p.i  
d3g ¡�p<g ƒ|d4< | ¡ �pPi  
d5g ¡�p^g D ]Z}t| 
d6¡�p€| 
d7¡�p€dƒ}|�r`|d8  | 
d¡|d9œ¡ |d:  t|ƒ7  < �q`| ¡ �pÈi }| 
d;¡|d<< W nH t�y" } z.|d2 �pødd=t|ƒj› � |d2< W Y d}~n
d}~0 0 |S )>aš  State of the ARMED maker: is it quoting, is it filling, what does it hold?

    THE FAILURE MODE IS SILENT. Every cap in kalshi_maker fails CLOSED -- if the
    exchange cannot be read, collateral reads as infinite and every order is
    refused. That is the safe direction, but it stops quoting without raising
    anything, and "no fills" then looks exactly like a quiet market. So the
    headline here is the AGE OF THE LAST SUCCESSFUL POST, not whether a process
    exists.

    Exchange state is read live rather than from our own logs: the point of a
    monitor is to catch the case where what we believe and what is actually
    resting have diverged.
    r   Nc                    s<   t | dƒ ¡ }d|v r"| dd¡}ˆ  ˆ |jddd�¡¡S )aP  Parse a maker CSV that may contain NUL bytes from an interrupted write.

        WHY: an interrupted write leaves a block of NULs mid-file, and
        csv.reader raises "line contains NUL" the moment it reaches them. That
        aborted this whole function partway through, so last_post_min kept only
        the value from rows BEFORE the corruption and legs_posted was never
        assigned at all -- the panel reported "last post 25863m ago, 0 legs
        posted" while the maker was quoting normally with 14 orders resting on
        the exchange beside it.

        The corruption is from the 2026-08-17 machine crash: 303 NULs in
        mm_live_orders.csv and 456 in mm_live_legs.csv, both immediately after
        a row stamped 2026-08-17T15:24. The panel had been frozen on that date
        for eighteen days. It fails toward "looks broken" rather than "looks
        fine", which is why it went unnoticed rather than unreported.

        research/mm_fill_dataset._read already does exactly this, which is why
        the research pipeline was never affected. Same treatment here.
        Úrbó    r,   r   Úreplace©Úerrors)rq   Úreadr–  rs   ÚStringIOrŽ  )ru   rF  ©rC  Ú_ior   r   Ú_rows�  s    zlive_maker.<locals>._rowsr    )Úerrr3   rK   Úexposurer/   Úlast_post_minÚ	placed_1hÚ
rejects_1hÚlegs_postedÚlegs_filledr^  Ústrand_rateÚstrand_nÚstrand_touchedz%E:\soccer\research\mm_live_orders.csvrv   r  r  ÚactionZplacedrS   rI  r   é<   r¡  r   )Zrejectr   i  r!  rb   zpost only crossZ
collateralZcapr:   zown capú:é(   r¢  r£  r]  iøÿÿÿr^  c                 S   s   h | ]}|  d ¡’qS rR   r)   r�   r   r   r   rV   Ö  r,   zlive_maker.<locals>.<setcomp>r¤  r   r–  )rm   r   r˜  c                 S   s>   g | ]6}|  d ¡dkr|  d¡dkr|  d¡p0dtkr|‘qS )rº   rM   Z	has_takerÚ0Zts_openrb   )r*   ÚSTRAND_SINCEr�   r   r   r   r‘   ë  s   þzlive_maker.<locals>.<listcomp>c                 S   s   g | ]}|  d ¡dkr|‘qS )ÚshapeZunfilledr)   r�   r   r   r   r‘   î  r,   c                 S   s   g | ]}|  d ¡dkr|‘qS )r®  Zstrandedr)   r�   r   r   r   r‘   ï  r,   r§  r¦  rO   r¥  r_  rž  r<   r3   r8   r9   ZpositionrK   )rS   r\   rŸ  Zbalance_dollarsr/   z exchange: )rr   Úior   rþ   r   Úutcr   r;  r’   r@   r*   rY  r   r=  r‡  rZ  ÚsplitrA   rf   rq   ÚBOOK_CSVrs   rf  rg  r>   Zget_open_ordersrg   r"   rC   ÚabsZget_balance)r�  rF   rþ   r   Úseenrx   r  ZactÚageÚdr.   r®   rƒ   rw   Z_bkZ_touchedÚ_strrE   Z_ktÚposrI   rG   r   r›  r   Ú
live_maker|  sŠ    ý





4&
.$  8r¹  c                 C   s’   | pd  d¡}|r|d nd}| d¡r4|dd… }| d¡rJ|dd… }t|ƒd	kr^|d	 nd}t|ƒd
krŠ|dd…  ¡ rŠ|d
d… }||fS )z3KXLALIGAGAME-26AUG20RVCALA -> ('LALIGA', 'RVCALA').rb   rW   r   ZKXr  NZGAMEéüÿÿÿr   é   )r±  Ú
startswithr�  rA   Úisdigit)r  Úpartsr§   Úteamsr   r   r   Úpretty_event  s    

rÀ  c                 C   sJ  ddl }ddl}g ddddddœ}tdƒ}z2| ¡  dd¡}t| | |jddd	�¡¡ƒ}W n< t	y˜ } z$t
|ƒdd
… |d< |W  Y d}~S d}~0 0 dd„ }t tj¡}	g g  }
}g }|D �]}| d¡pÒd}zt |¡}W n t� yú   d}Y n0 |�r|	|  ¡ d nd}|du�r:|dk�r:|d  d7  < || d¡ƒ|| d¡ƒ }}|du�rl|
 |¡ |du�r€| |¡ t| d¡ƒ\}}| |||||| d¡�p®d|| d¡ƒ|| d¡ƒ||dœ
¡ qÀt|ƒ|d< |
�r|
 ¡  |
t|
ƒd  |d< |�r$| ¡  |t|ƒd  |d< |jdd „ d!d"� |d| … |d#< |S )$z;Real maker fills, newest first, from mm_live_realfills.csv.r   N)rƒ   Ún_totalÚn24Úmed_secsÚ	med_queuer   r]  r•  r,   r   r–  r—  r   r   c              	   S   s(   z
t | ƒW S  ttfy"   Y d S 0 d S r   r!   r%   r   r   r   r´   /  s    
zlive_fills.<locals>._fr  rb   g      ¬@rà   rÂ  r   Zseconds_to_fillZqueue_ahead_at_postrn   r  Zprice_centsrˆ  )
r  ÚwhenÚage_hrÅ   r¿  r  Zpricer\   ÚsecsZqueuerÁ  r  rÃ  rÄ  c                 S   s   | d S )Nr  r   r%   r   r   r   r+   W  r,   zlive_fills.<locals>.<lambda>T©r.   Úreverserƒ   )rr   r¯  r   r†  r–  rf   rs   rš  rŽ  rt   r@   r   rþ   r   r°  r*   rY  r$   rZ  rC   rÀ  rA   r�   )r;   rC  rœ  rF   r   rF  rƒ   rE   r´   rþ   rÇ  ZqueuesZparsedrx   r  rÅ  rÆ  rÞ   rI   r§   r¿  r   r   r   Ú
live_fills   sf    ÿÿ







ú
	rÊ  c                     sœ  t td i ƒ} t td i ƒ}zdd l}|j}W n tyD   i }Y n0 zdd l}|j}W n typ   tƒ }Y n0 tdd„ ƒ}�zdd l	}t
 ¡ t
 ¡ tdd� fD ]è}t| ¡ › d� }	|	 ¡ sÄq¤t|	d	d
d��¨}
| |
¡D ]Œ}| d¡pîd	dkröqÞ| d¡�pd}dD ]^\}}z:| |¡dv�rJt|| ƒdk�rJ|| |  d7  < W n ttf�yd   Y n0 �q
qÞW d   ƒ q¤1 �s‚0    Y  q¤W n t�y¤   Y n0 g }t|  ¡ ƒD ]à\‰ }t|tƒ�sÎ�q¶t‡ fdd„| ¡ D ƒƒ}t‡ fdd„| ¡ D ƒƒ}| ˆ dddddœ¡}t| d¡ƒ}t| d¡ƒ}ˆ |v }|�rN|�rNd}n |�rj|�rjˆ |v �rjd}nd }| ˆ | dd	¡|||||||dœ	¡ �q¶|S )Nzleague_map.jsonzteam_map.jsonr   c                   S   s   dddddœS )Nr   ©ÚpinrM   ÚpolyrN   r   r   r   r   r   r+   m  r,   zleague_health.<locals>.<lambda>r   rÙ   z.csvrb   r   rl   r:   ZPRErÅ   rµ   ))rÌ  Zpin_home)rM   Zkalshi_home_mid)rÍ  Zpoly_home_mid)rN   Zpolyus_home_midr  c                 3   s*   | ]"}t |tƒr| d ¡ˆ krdV  qdS )r}   r   N©Ú
isinstancerB  r*   rM  ©Úcoder   r   rß   ‡  s   ÿz league_health.<locals>.<genexpr>c                 3   s4   | ],}t |tƒr| d ¡ˆ kr| d¡rdV  qdS )r}   rM   r   NrÎ  rM  rÐ  r   r   rß   ‰  s   "ÿrË  Zkalshi_seriesZpoly_tagrî   Zxlow_polyusra   )	rÑ  ra   Úk_seriesrÍ  rN   r¿  Úabbrrv   Úcan_fire)r   ro   Zpoly_us_pricesZLEAGUE_SLUGr   Zlive_monitorZGAP_LEAGUESr’   r   rr   r   rü   r   ÚLIVErý   r;  rq   rs   r*   r"   r#   r$   rB   ri   rÏ  rB  rd   re   ÚboolrC   )ÚlmZtmZpupZpus_mapZlmonr  r´  rC  r¶  r   rw   rx   r§   r‘  r’  rF   ÚinfoZn_teamsZn_abbrrÞ   Zhas_k_seriesZhas_polyZhas_pusrÔ  r   rÐ  r   Úleague_health^  sl    


"2ürÙ  uò*  
/* Dark is the default surface (this page has always been dark and is read at
   night); light is a SELECTED alternate with its own steps, not an auto-inverted
   copy -- flipping a dark palette produces muddy status colours. Both themes
   keep the same semantic tokens so nothing downstream needs to know which is on. */
:root{--bg:#12131a;--card:#1b1d26;--line:#2c2f3d;--ink:#e8e9ee;--dim:#9aa0b4;
--good:#4ec27f;--bad:#e56b6b;--warn:#e0a94a;--acc:#5b9dd9;--hover:#232635}
:root[data-theme="light"]{--bg:#f6f7f9;--card:#ffffff;--line:#e2e5ea;--ink:#1a1c22;
--dim:#63697a;--good:#1a7f37;--bad:#c1272d;--warn:#9a6700;--acc:#2f6fb0;--hover:#f0f2f6}
*{box-sizing:border-box}
body{margin:0;padding:18px;background:var(--bg);color:var(--ink);
font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
-webkit-text-size-adjust:100%}
h1{font-size:19px;margin:0 0 2px}
.sub{color:var(--dim);font-size:12px;margin-bottom:14px}
.topbar{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
.tools{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
button.tool{background:var(--card);color:var(--dim);border:1px solid var(--line);
border-radius:7px;padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
button.tool:hover{background:var(--hover);color:var(--ink)}
input.filter{background:var(--card);color:var(--ink);border:1px solid var(--line);
border-radius:7px;padding:6px 10px;font:inherit;font-size:12px;min-width:150px}
input.filter::placeholder{color:var(--dim)}
.kpis{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:6px}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:9px;
padding:11px 14px;min-width:126px;flex:1}
.kpi .lbl{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.04em}
.kpi .val{font-size:19px;font-weight:600;margin-top:3px}
/* Sections collapse so a long page can be skimmed. <details>/<summary> is used
   rather than JS toggling so it still works with scripting disabled. */
details.sec{margin:22px 0 0}
details.sec>summary{font-size:15px;font-weight:600;cursor:pointer;list-style:none;
padding:6px 0;display:flex;align-items:center;gap:8px;color:var(--ink)}
details.sec>summary::-webkit-details-marker{display:none}
details.sec>summary::before{content:'\25B8';color:var(--dim);font-size:11px;
transition:transform .15s;display:inline-block}
details.sec[open]>summary::before{transform:rotate(90deg)}
details.sec>summary:hover{color:var(--acc)}
.count{color:var(--dim);font-weight:400;font-size:13px}
/* Nested drill-down. Its own class so the page-level expand/collapse button
   (which targets details.sec) does not toggle these too -- opening every book
   list at once buries the summary the section exists to show. */
details.sub{margin:8px 0 0 4px}
details.sub>summary{font-size:13px;font-weight:500;cursor:pointer;list-style:none;
padding:5px 0;color:var(--dim);display:flex;align-items:center;gap:7px}
details.sub>summary::-webkit-details-marker{display:none}
details.sub>summary::before{content:'\25B8';font-size:10px;transition:transform .15s;
display:inline-block}
details.sub[open]>summary::before{transform:rotate(90deg)}
details.sub>summary:hover{color:var(--acc)}
.card{background:var(--card);border:1px solid var(--line);border-radius:9px;
overflow-x:auto;margin-bottom:6px}
table{border-collapse:collapse;width:100%;min-width:520px}
th{text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.04em;
color:var(--dim);font-weight:600;padding:9px 11px;border-bottom:1px solid var(--line);
white-space:nowrap;position:sticky;top:0;background:var(--card);z-index:1}
th.sortable{cursor:pointer;user-select:none}
th.sortable:hover{color:var(--ink)}
th.sortable::after{content:'\2195';opacity:.28;margin-left:5px;font-size:10px}
th.sortable.asc::after{content:'\2191';opacity:.9}
th.sortable.desc::after{content:'\2193';opacity:.9}
td{padding:9px 11px;border-bottom:1px solid var(--line);white-space:nowrap}
tbody tr:hover td{background:var(--hover)}
tr:last-child td{border-bottom:none}
.good{color:var(--good)}.bad{color:var(--bad)}.warn{color:var(--warn)}.dim{color:var(--dim)}
.pill{display:inline-block;padding:2px 7px;border-radius:99px;font-size:11px;
border:1px solid var(--line);color:var(--dim)}
.pill.k{border-color:#3d5a80;color:#8fb8e0}
.pill.p{border-color:#4a3d6b;color:#b49ae0}
:root[data-theme="light"] .pill.k{border-color:#9dbbdc;color:#255d99}
:root[data-theme="light"] .pill.p{border-color:#bcabdf;color:#5b3fa0}
/* z-gate SHADOW marker. Warm, not alarming: the row is still a real signal
   that still counts in the frozen forward test -- the pill says a shadow rule
   would have skipped it, which is a note, not a warning. Deliberately NOT
   --bad, which on this page means "lost money". */
.pill.z{border-color:var(--warn);color:var(--warn)}
tr.zdrop td{opacity:.72}
.grouphdr td{background:var(--hover);border-top:2px solid var(--line);
font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--dim);
font-weight:600;padding:7px 11px}
tbody tr.grouphdr:hover td{background:var(--hover)}
.bar{height:6px;background:var(--line);border-radius:99px;overflow:hidden;margin-top:7px}
.bar>i{display:block;height:100%;background:var(--acc)}
.note{color:var(--dim);font-size:12px;margin:6px 0 0}
tr.hidden{display:none}
.empty{color:var(--dim);font-size:12px;padding:10px 11px}
/* â”€â”€ Equity curve â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   One series per chart, so no legend -- the heading names it. Sign is carried
   GEOMETRICALLY, by position against the dashed zero line, because the good/bad
   pair separates by only ~4.9 deutan dE: colour here reinforces a cue that is
   already there, it is never the only one. Marks stay thin and the axes
   recessive so the line is the loudest thing in the figure. */
.eq{width:100%;height:auto;display:block}
.eq .eq-line{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
/* SECOND series (a shadow variant drawn against the real one). Distinguished
   by DASH, not by hue -- same reason the sign is encoded by position: a reader
   who cannot separate the two colours still sees solid vs dashed. Thinner and
   dimmer so the rule that is actually live stays the loudest line. */
.eq .eq-line2{fill:none;stroke-width:1.5;stroke-dasharray:4 3;opacity:.85;
  stroke-linejoin:round;stroke-linecap:round}
.eq .eq-dot2{stroke:var(--card);stroke-width:2}
.eqkey{display:flex;gap:12px;flex-wrap:wrap;margin:-2px 0 6px;
  color:var(--dim);font-size:11px;text-transform:none;letter-spacing:0}
.eqkey i{display:inline-block;width:14px;height:0;vertical-align:middle;
  border-top-width:2px;margin-right:5px}
/* the zero rule is the reference the whole chart is read against, so it is the
   one piece of chrome allowed to be visible rather than merely present */
.eq .eq-zero{stroke:var(--dim);stroke-width:1;stroke-dasharray:3 3;opacity:.55}
.eq .eq-ax{fill:var(--dim);font-size:10px}
.eq .eq-end{fill:var(--ink);font-size:11px;font-weight:600}
.eq .eq-cross{stroke:var(--dim);stroke-width:1;stroke-dasharray:2 2}
.eq .eq-dot{stroke:var(--card);stroke-width:2}
.eq .eq-tip{fill:var(--ink);font-size:11px;font-weight:600}
.eq .eq-tipbg{fill:var(--card);stroke:var(--line);stroke-width:1;rx:4}
/* The backed side, shown IN the match rather than as its own column. Position
   already says home or away (before or after "vs"), so this only has to mark
   WHICH team -- one accent, no colour code to learn, and it cannot be confused
   with the good/bad used for outcomes. */
.pick{color:var(--acc);font-weight:600}
/* Provenance dot: this result came from a settled market, not the match record.
   Deliberately faint -- it is a footnote about where a number came from, not a
   warning about the number. */
.prov{opacity:.45;margin-left:3px;cursor:help}
/* Age of the build, filled in by the browser. Quiet when fresh; red once the
   page has missed two publish cycles, which is the only case worth a colour. */
.age{opacity:.7}
.age.stale{color:#e5534b;opacity:1;font-weight:600}
/* CLV rides with its ask: they are one thought -- what it costs and how far
   below fair that is -- and two columns for it cost width a phone has not got.
   Inline on a wide screen, stacked under the price on a narrow one. */
.clvsub{margin-left:7px}
.eqwrap{background:var(--card);border:1px solid var(--line);border-radius:9px;
padding:10px 12px 6px;margin:0 0 8px}
.eqhead{color:var(--dim);font-size:11px;text-transform:uppercase;
letter-spacing:.04em;margin-bottom:2px}

/* â”€â”€ Mobile â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Wide tables shed DETAIL rather than forcing a sideways scroll: columns are
   tagged by priority and drop out as the screen narrows, so what survives on a
   phone is the answer (which match, which side, what price, what happened) and
   what goes is the supporting evidence. Horizontal scroll still exists as a
   fallback for the tables that stay wide.
     .opt   secondary  -> hidden below 900px
     .opt2  tertiary   -> hidden below 640px  */
@media(max-width:900px){
  .opt{display:none}
  table{min-width:0}
}
@media(max-width:640px){
  body{padding:12px}
  .kpi{min-width:calc(50% - 5px)}
  .topbar{flex-direction:column;align-items:stretch}
  .tools{width:100%}
  input.filter{flex:1}
  .opt2{display:none}
  h1{font-size:18px}
  /* long match names wrap instead of stretching the row off-screen */
  td{white-space:normal}
  /* Reclaim the last ~40px from CHROME rather than from content. At 11px of
     horizontal padding a five-column table spends ~110px on gutters; 6px still
     reads as separated and buys back a whole column's worth of width. */
  th,td{padding:8px 6px;font-size:13px}
  th{font-size:11px}
  /* The match is the widest column and, left alone, the table's auto layout
     gives it whatever the longest fixture name asks for. The cap has to sit on
     an INNER inline-block: max-width on a <td> is treated as a suggestion by
     the auto table algorithm and was being ignored outright. Two short lines
     cost height, which scrolls naturally, instead of width, which does not. */
  .mcell{white-space:normal}
  .mw{display:inline-block;max-width:112px}
  /* kickoff stacks UNDER the date instead of beside it: same information, ~35px
     narrower, and height is the axis that scrolls for free. Same trick for the
     CLV under its ask. */
  .kotime{display:block}
  .clvsub{display:block;margin-left:0}
  th{white-space:normal}
  td,th{padding:8px 7px;font-size:13px}
  /* touch targets: summaries and buttons get real height */
  details.sec>summary{padding:10px 0}
  details.sub>summary{padding:9px 0}
  button.tool{padding:9px 12px}
}
@media print{button.tool,input.filter{display:none}details.sec{display:block}}
uÞ  
(function(){
  // Theme: honour the OS preference, remember an explicit choice.
  var root=document.documentElement, KEY='xlow-theme';
  var saved=null; try{saved=localStorage.getItem(KEY);}catch(e){}
  // Dark is the default regardless of OS preference -- this page is read on a
  // dark terminal setup and its palette was built for the dark surface first.
  // An explicit choice still wins and persists.
  root.setAttribute('data-theme', saved || 'dark');
  var tbtn=document.getElementById('themeBtn');
  function label(){ tbtn.textContent = root.getAttribute('data-theme')==='light'?'Dark':'Light'; }
  if(tbtn){ label(); tbtn.onclick=function(){
    root.setAttribute('data-theme', root.getAttribute('data-theme')==='light'?'dark':'light');
    try{localStorage.setItem(KEY, root.getAttribute('data-theme'));}catch(e){}
    label(); }; }

  // Sort. Numeric where the whole column parses as a number (after stripping
  // %,$,+,commas) so "-14.7%" and "$1,797.89" sort as magnitudes, not strings.
  function val(td){
    // An explicit data-sort wins. Needed where the DISPLAY is not the sort key:
    // "A Â· +$34.05" would otherwise parse to nothing and fall back to sorting
    // alphabetically by result letter, which is not what anyone means by
    // "sort the result column".
    if(td.dataset && td.dataset.sort!==undefined && td.dataset.sort!==''){
      var d=parseFloat(td.dataset.sort);
      return isNaN(d)?null:d;
    }
    var t=(td.textContent||'').trim();
    var n=parseFloat(t.replace(/[$,%+\s]/g,'').replace(/^\((.*)\)$/,'-$1'));
    return isNaN(n)?null:n;
  }
  document.querySelectorAll('table').forEach(function(tb){
    var head=tb.tHead||tb.rows[0]&&tb.rows[0].parentNode;
    var hr=tb.querySelector('tr'); if(!hr) return;
    var body=tb.tBodies[0]||tb;
    Array.prototype.forEach.call(hr.cells,function(th,i){
      if(th.tagName!=='TH') return;
      th.classList.add('sortable');
      th.onclick=function(){
        var all=Array.prototype.slice.call(body.rows);
        // Rows marked .nosort belong to a different rule (or are separators).
        // They keep their original order and stay BELOW the sorted block --
        // sorting them in with the primary implies a pooling that is wrong.
        var pinned=all.filter(function(r){ return r.classList.contains('nosort'); });
        var rows=all.filter(function(r){
          return r!==hr && !r.classList.contains('nosort')
                 && r.cells.length===hr.cells.length; });
        if(!rows.length) return;
        var dir = th.classList.contains('asc')?-1:1;
        Array.prototype.forEach.call(hr.cells,function(o){o.classList.remove('asc','desc');});
        th.classList.add(dir===1?'asc':'desc');
        rows.sort(function(a,b){
          var x=val(a.cells[i]), y=val(b.cells[i]);
          if(x!==null&&y!==null) return (x-y)*dir;
          return (a.cells[i].textContent.trim()).localeCompare(b.cells[i].textContent.trim())*dir;
        });
        rows.forEach(function(r){ body.appendChild(r); });
        pinned.forEach(function(r){ body.appendChild(r); });
      };
    });
  });

  // Filter: hides non-matching rows across every table at once, so typing a
  // league code or team narrows the whole page rather than one section.
  var f=document.getElementById('filterBox');
  if(f){ f.oninput=function(){
    var q=f.value.toLowerCase().trim();
    document.querySelectorAll('table').forEach(function(tb){
      var hr=tb.querySelector('tr');
      Array.prototype.forEach.call(tb.rows,function(r){
        if(r===hr) return;
        if(r.cells.length===1){ return; }   // keep spanning note rows
        r.classList.toggle('hidden', !!q && r.textContent.toLowerCase().indexOf(q)<0);
      });
    });
    if(q) document.querySelectorAll('details.sec').forEach(function(d){d.open=true;});
  }; }

  // Equity hover: crosshair + tooltip. A line chart that cannot be read at a
  // point is half a chart -- the end label alone tells you where it finished,
  // not when it drew down. Nearest-index by pixel x, so the whole plot area is
  // a hit target rather than each 2px vertex.
  document.querySelectorAll('svg.eq').forEach(function(svg){
    var xs=(svg.getAttribute('data-x')||'').split(',').map(Number),
        vs=(svg.getAttribute('data-v')||'').split(',').map(Number),
        ds=(svg.getAttribute('data-d')||'').split(','),
        g=svg.querySelector('.eq-hov'), hit=svg.querySelector('.eq-hit');
    if(!xs.length||!g||!hit) return;
    var cross=g.querySelector('.eq-cross'), dot=g.querySelector('.eq-dot'),
        tip=g.querySelector('.eq-tip'), bg=g.querySelector('.eq-tipbg'),
        line=svg.querySelector('.eq-line'), pathEl=line&&line.getAttribute('d');
    // second series, when the chart has one. Same index space as the first --
    // equity_svg guarantees they are sampled on the same bet sequence -- so
    // one nearest-index lookup serves both.
    var v2raw=svg.getAttribute('data-v2')||'',
        v2=v2raw?v2raw.split(',').map(Number):null,
        dot2=g.querySelector('.eq-dot2'),
        line2=svg.querySelector('.eq-line2'),
        pathEl2=line2&&line2.getAttribute('d');
    function segAt(d,i){
      // y comes from the path string rather than being re-emitted: one source
      var m=d.split(' L'); var seg=(i===0?m[0].slice(1):m[i]);
      return seg.split(',').map(Number);
    }
    function ptAt(i){ return segAt(pathEl,i); }
    function move(ev){
      var r=svg.getBoundingClientRect();
      var sx=(ev.clientX-r.left)*(640/r.width);
      var best=0, bd=1e9;
      for(var i=0;i<xs.length;i++){ var d=Math.abs(xs[i]-sx); if(d<bd){bd=d;best=i;} }
      var p=ptAt(best);
      g.style.display='';
      cross.setAttribute('x1',p[0]); cross.setAttribute('x2',p[0]);
      dot.setAttribute('cx',p[0]); dot.setAttribute('cy',p[1]);
      var txt=ds[best]+'  '+(vs[best]>=0?'+':'')+vs[best].toFixed(2);
      if(v2&&pathEl2&&dot2){
        var p2=segAt(pathEl2,best);
        dot2.setAttribute('cx',p2[0]); dot2.setAttribute('cy',p2[1]);
        dot2.style.display='';
        txt+='   /  '+(v2[best]>=0?'+':'')+v2[best].toFixed(2);
      }
      tip.textContent=txt;
      var w=txt.length*6.2+10, tx=Math.min(Math.max(p[0]-w/2,2),640-w-2);
      bg.setAttribute('x',tx); bg.setAttribute('y',2); bg.setAttribute('width',w);
      tip.setAttribute('x',tx+w/2); tip.setAttribute('y',14);
      tip.setAttribute('text-anchor','middle');
    }
    hit.addEventListener('mousemove',move);
    hit.addEventListener('touchmove',function(e){
      if(e.touches&&e.touches[0]){ move(e.touches[0]); e.preventDefault(); }
    },{passive:false});
    hit.addEventListener('mouseleave',function(){ g.style.display='none'; });
    hit.addEventListener('touchend',function(){ g.style.display='none'; });
  });

  // Default sort, applied only to tables that ASK for it via data-defaultsort.
  // Not blanket: "Recent books" is deliberately newest-first and the regime
  // table is in v1..v4 order, so sorting every table by column 0 would undo
  // orderings that were chosen on purpose.
  document.querySelectorAll('table[data-defaultsort]').forEach(function(tb){
    var i=parseInt(tb.getAttribute('data-defaultsort'),10)||0;
    var hr=tb.querySelector('tr');
    if(hr && hr.cells[i] && hr.cells[i].onclick) hr.cells[i].onclick();
  });

  var eb=document.getElementById('expandBtn');
  if(eb){ eb.onclick=function(){
    var any=Array.prototype.some.call(document.querySelectorAll('details.sec'),
      function(d){return !d.open;});
    document.querySelectorAll('details.sec').forEach(function(d){d.open=any;});
    eb.textContent = any?'Collapse all':'Expand all';
  }; }
})();
c                 C   s    t | ƒ dd¡ dd¡ dd¡S )Nú&z&amp;ú<z&lt;ú>z&gt;)r@   r–  )rÞ   r   r   r   r�  ù  s    r�  r   c                 C   sD   | d u rdS | dkrdn| dk r$dnd}d|› d| d|› d	�›d
�S )Nõ   <span class="dim">â€”</span>r   ÚgoodÚbadÚdimú<span class="rs  ú+.r®   ú%</span>r   ©r  ÚdigitsÚclsr   r   r   Úpctý  s    rç  c              	   C   sV   zt t| ƒ dd¡ƒ}W n ttfy0   Y dS 0 t|d |rN|jd |j nd ƒS )a´  Numeric sort key for a Date cell: YYYYMMDDHHMM.

    The table sorter parses cell TEXT as a number, and "08-14" parses to 8 --
    so every row in the same month tied and the Date column silently did
    nothing at all. Adding the kickoff did not break it; it made an existing
    tie visible. data-sort is the documented escape hatch for a cell whose
    display is not its sort key, so the column now orders by date THEN kickoff.
    rW   rb   i'  r  r   )r<  r@   r–  r#   r$   ZhourZminute)Zdatestrrá   r¶  r   r   r   Údate_sort_key  s
    	rè  r  c                 C   sD   | d u rdS | dkrdn| dk r$dnd}d|› d| d|› d	�›d
�S )NrÝ  r   rÞ  rß  rb   rá  ú">$ú,.r®   ú</span>r   rä  r   r   r   Úmoney  s    rì  c                 C   s    | dk rdnd› dt | ƒd›�S )z}Axis money label. The sign goes OUTSIDE the currency symbol: "$-106" is
    the kind of thing that gets read as $106 at 10px.r   rW   rb   ú$ú,.0f)r³  ©r  r   r   r   Ú	_ax_money  s    rð  c              [      s|  t | ƒdk rdS | d d dfgt| ƒ } dd„ | D ƒ‰d}|rht |ƒt | ƒd krhdgd	d„ |D ƒ }tˆ|prg  dg ƒtˆ|p†g  dg ƒ ‰‰ˆˆ d
k rªˆd ‰d\}}d\‰ }‰}	|ˆ  | |ˆ |	  ‰‰‡ ‡‡fdd„‰‡‡‡‡fdd„‰‡‡fdd„tˆƒD ƒ}
dd dd„ |
D ƒ¡ }ˆdƒ}ˆd dk�rDdnˆd dk �rVdnd}|dk�rpd|› d�nd}|
d \}}d dd„ |
D ƒ¡}d d d„ ˆD ƒ¡}d d!d„ | D ƒ¡}d }}d}d"}|du�rÔ‡‡fd#d„t|ƒD ƒ}dd d$d„ |D ƒ¡ }|d \}}d d%d„ |D ƒ¡}t|| ƒd&k�r<d'n||k�rJd(nd)}d*|› d+|› d,|d-›d.|d-›d/|› d0|d1 d-›d2|| d-›d3|› d4|d d5›d6�}|�p¨d7\}}d8|› d9t|ƒ› d:|› d9t|ƒ› d;�	}d<t|ƒ› d=|› d>|d?›d@|d?›dAt|ƒ› dBt ˆƒ› dCˆd dD›dE|› dF|› dG|› dH|› dI|› dJˆ › dK|d-›dL|| d-›dM|d-›dN|› dO|› d+|› d,|d-›d.|d-›dP|› dQˆ dR d?›d2ˆˆƒdS d-›dTtˆƒ› dUˆ dR d?›d2ˆˆƒdS d-›dTtˆƒ› dUˆ d?›d2|dR d?›d4t| d d ƒ› dU|| d-›d2|dR d?›dTt| d d ƒ› dV|d1 d-›d2|dS d-›d4ˆd d5›dWˆ› dMˆˆ d-›dX|› dY|› dZˆ › d2ˆ› d[ˆd-›d\ˆd-›d]�[S )^aU  Inline SVG equity curve for one paper rule, optionally with a variant.

    ONE series by default, and then there is no legend -- the heading names it,
    and a legend box for a single line is noise. Sign is encoded by POSITION
    against a dashed zero line, not by hue: --good and --bad separate by only
    ~4.9 deutan dE, so a reader who cannot tell them apart still sees the curve
    sitting below zero. Colour reinforces; geometry and the end label carry it.

    `overlay` adds a SECOND series and, with it, the legend the single-series
    case does not need. It must be sampled on the SAME bet sequence as
    `points` -- same length, same dates, one entry per bet of the primary rule
    -- because the two are meant to be read as "what the same run would have
    looked like". A variant that skips a bet carries its previous level
    forward, so the dashed line going flat IS the variant sitting that bet out.
    Building it on its own shorter sequence instead would silently compress its
    x-axis and make two different timelines look like one.

    The variant is separated by DASH rather than hue, for the same reason the
    sign is: solid vs dashed survives any colour vision.

    `points` is [(game_date, cumulative_units)] in bet order; units are scaled
    by PAPER_NOTIONAL here so the curve cannot drift from the table or the tile.
    r  rb   r   r    c                 S   s   g | ]\}}|t  ‘qS r   ©ÚPAPER_NOTIONAL©rT   r  r  r   r   r   r‘   ?  r,   zequity_svg.<locals>.<listcomp>Nr   c                 S   s   g | ]\}}|t  ‘qS r   rñ  ró  r   r   r   r‘   B  r,   g•Ö&è.>r¢   )g      „@g      ^@)g      G@g      O@r	   g      4@c                    s   ˆ ˆ|  t ˆƒd   S rÖ   r×   )Úi)ÚLÚpwÚvalsr   r   r°   J  s    zequity_svg.<locals>.pxc                    s   ˆ ˆˆ|   ˆˆ   S r   r   rï  )r'  ÚhiÚloÚphr   r   ÚpyM  s    zequity_svg.<locals>.pyc                    s    g | ]\}}ˆ |ƒˆ|ƒf‘qS r   r   ©rT   rô  r  ©r°   rû  r   r   r‘   P  r,   ÚMz Lc                 s   s$   | ]\}}|d ›d|d ›�V  qdS ©ru  r  Nr   ©rT   r&   Úyr   r   r   rß   Q  r,   zequity_svg.<locals>.<genexpr>rX   rÞ  rß  rà  zvar(--ú)z
var(--dim)r  c                 s   s   | ]\}}|d ›V  qdS )ru  Nr   )rT   r&   r  r   r   r   rß   W  r,   c                 s   s   | ]}|d ›V  qdS ©ú.2fNr   rM  r   r   r   rß   X  r,   c                 s   s   | ]\}}t t|ƒƒV  qd S r   )r�  r@   )rT   r"  r  r   r   r   rß   Y  r,   z
var(--acc)c                    s    g | ]\}}ˆ |ƒˆ|ƒf‘qS r   r   rü  rý  r   r   r‘   _  r,   c                 s   s$   | ]\}}|d ›d|d ›�V  qdS rÿ  r   r   r   r   r   rß   `  r,   c                 s   s   | ]}|d ›V  qdS r  r   rM  r   r   r   rß   b  r,   é   g      @g      *@g      Àz<path class="eq-line2" d="z
" stroke="z"/><circle cx="ru  z" cy="z" r="3" fill="z"/><text class="eq-end" x="r(  z" y="z" fill="rs  z+,.0fz</text>)ZruleZvariantz8<div class="eqkey"><span><i style="border-top:2px solid z"></i>z-</span><span><i style="border-top:2px dashed z</span></div>z(<div class="eqwrap"><div class="eqhead">ry  z<svg class="eq" viewBox="0 0 rw  ú z" role="img" aria-label="r_  z bets, ending z+,.2fz dollars paper" data-eq="z
" data-x="z
" data-v="z" data-v2="z
" data-d="z"><line class="eq-zero" x1="z" y1="z" x2="z" y2="z"/>z<path class="eq-line" d="z" r="4" fill="z"/><text class="eq-ax" x="rr  r«   z" text-anchor="end">z</text><text class="eq-ax" x="z</text><text class="eq-end" x="zI</text><g class="eq-hov" style="display:none"><line class="eq-cross" y1="z&"/><circle class="eq-dot" r="4" fill="z'"/><circle class="eq-dot2" r="3" fill="zf" style="display:none"/><rect class="eq-tipbg" rx="4" height="17"/><text class="eq-tip"/></g><rect x="z	" width="z
" height="zJ" fill="transparent" class="eq-hit" style="cursor:crosshair"/></svg></div>)	rA   rf   rA  r‡  Ú	enumerater@  r³  r�  rð  )ZpointsZcidrc   ÚoverlayÚkeysZvals2ÚWrŸ   ÚRÚBZptsÚlineÚbaseræ  r’  ÚexZeyZdxZdvZddZsvg2r.   Zdv2Zcol2Zpts2Zline2Zex2Zey2Z_dyZk1Zk2r   )	rõ  r'  rø  rù  rú  rö  r°   rû  r÷  r   Ú
equity_svg!  s$   .(
(ÿÿÿþþýýÿÿþþÿÿþþþýýýüüøøøøõôôóóóððïîîíìììëëêéééççæåããããÿr  c            ­   !      sØ(  t  ¡ } | tdd� }tƒ }tƒ }tƒ }tt|  ¡ | ¡ h| 	d¡d�|ƒ\}}t
ƒ }|d d }|d d }	tdd	„ |D ƒƒtdd	„ |	D ƒƒ }
td
d	„ |	D ƒƒ}tdd	„ |	D ƒƒ}ddddddt› d�dg}| d¡ | d¡ | dt ¡ jdd�› dt ¡  d¡› d|d › d�¡ | d¡ | d¡ | d ¡ | d!¡ | d"¡ | d#¡ |d$  	d%¡}| d&¡ |d }| 	d'¡d u �r”| d(¡ np| 	d)¡�p¢i }d* d+d	„ t| ¡ ƒD ƒ¡}| d,t|d' ƒ› d-t| 	d.d/¡ƒ› d0t| 	d1d/¡ƒ› d2|› d#�	¡ |d }| 	d'¡d u �r(| d3¡ n>| d4t|d' ƒ› d-t| 	d.d/¡ƒ› d0t| 	d1d/¡ƒ› d#�¡ | d5t|
ƒ› d6t|ƒt|	ƒ › d7�¡ |�r| d8t|d9 ƒ› d:|d; › d<|d= d>›d#�¡ |d? |d@  }| dAt|ƒ› d6|d? dB›dC|d@ dB›dD�¡ dE|| 	d¡ffD ]Ö\}}}|�r| 	d;¡�s4�q|d9 dF |d;  t }t| }|tk�rbdGn|tk �rpdHndI}| dJ|› dKt|ƒ› dL|› dM|dN›dOtdN›dP|d; › d*|d9 dQ›dR|d? dS›dC|d@ dS›dTtt|�pÚdUƒƒ› d#�¡ �q| dV¡ tƒ }|dW d u �p|dW dXk}|�r dYndZ}| d[|› d\|d] d u�rF|d] ndU› d^�¡ | d_¡ | d`¡ |dW d u �rxdan|dW dS›db�}| 	dc¡d u �rœddn"|dc dS›de|df › dg|dh › di�}| dj|�rÐdHndG› d|› dk|d] d u�rò|d] ndU› dl|dm › dk|dn › dk|do › dp|› dq|dr dS›dstt|d' �p8dUƒƒ› dt�¡ | du¡ | tƒ ¡ |dv �r¤d* dwd	„ t|dv  ¡ dxdy„ dz�D ƒ¡}| d{t|ƒ› d|�¡ |d �rÔ| d}d* d~d	„ |d D ƒ¡ dV ¡ | dt d€ ¡ | d�¡ | d‚¡ |�rdƒ|› d„�nd…} | 	d†¡�pi ‰t‡fd‡d	„|D ƒƒ}!|!�rHdƒ|!› dˆ�nd…}"| d‰t|ƒ› dŠ| › |"› d‹�¡ | 	dŒ¡�pzd }#| t| 	d�¡�p�g dŽd�| 	ddU¡› d�t| 	d�¡�p²g ƒ› d‘td’›d“�|#d”d•�¡ | d–¡ | d—¡ |�sò| d˜¡ d™}$d/}%d/}&g }'|D �]}dš d›d	„ |dœ D ƒ¡�p(d�}(|dž })|)dŸk�rD|d  n|d¡ }*|*d u �rt|d  d u�rl|d  n|d¡ }*d¢d£d¤œ 	|)d…¡}+d…},d }-|d¥ �rv|d¥ d¦ }.|.d u�rð|*�rðd/|*  k �rÌdk �rðn n |.d/k�ræd|* |* nd§t }/n|.}/|/d u�r,|/d¨›},|$|/7 }$|%d7 }%|/}-|/d/k�r,|&d7 }&|d¥  	d©¡�r@dªnd…}0|/d u�rZt|/ƒ|0 nt|d¥ d« �pldUƒ|0 }1nd¬}1|d­ dŽk�rŒd…nd®t|d­ ƒ› di�}2|-d u�r¾|*d u�r¾|' |*¡ |*d u �rÌd¯n"d°|*d±›d²�|+�rêd³|+› d´�nd… }3t|)dŸk�	r|dµ n|d¶ ƒ}4|d· d u �	r d¸n
|d· d¹›}5| 	dº¡�	p:i }6|6 	|d» |d¼ | 	d½¡| 	d¾¡f¡�	p€|6 	|d» |d¼ | 	d½¡f¡}7|7�	r–d¿|7dÀ›di�nd…}8t| 	d†¡�	pªi  	|d» ¡�	pºdÁƒ}9d…}:|d¼ |9v �	rÔdÂ}:t| 	d½¡�	pädUƒt| 	d¾¡�	pödUƒ };}<t| 	dÃ¡�
pd…ƒ  ¡ }=|= !dÄ¡�
r4dÅ|;› dÆ|<› �}>n*|= !dÇ¡�
rR|;› dÈ|<› di�}>nt|dÉ ƒ}>| dÊ|:�
rndËnd…› dÌt"|d» |7ƒ› dt|d» dXd … ƒ› |8› dÍt|d¼ ƒ› dÎ|>› |2› |:› dÏ|*d u�
rÈ|*nd…› d|3› dÐ|4› dÑ|5› dÍt|)dŸk�
rô|dµ n|d¶ ƒ› dÒ|(› dÓ|,› d|1› dt�¡ �q|%�r|$d/k�r0dGn|$d/k �r>dHndI}?dF|& |% }@|'�rhdFt|'ƒt|'ƒ  nd }A|Ad u �rŒdÔ|@dB›dÕ|&› dÖ�}BnR|@|A }C|Cd/k�r¢dGn|Cd/k �r°dHndI}DdÔ|@dB›dÕ|&› d×|AdB›dØ|D› d|CdQ›dÙ�}B| dÚ|%› dÛt|ƒ› |B› dÜ|?› dÝt|$ƒ› dÞ�
¡ | du¡ | dßttƒ› dà�¡ | dá¡ tdâdã„ | 	d†¡�pNi  #¡ D ƒƒ}E|E�r|| dätdå |E¡ƒ dæ ¡ | d‚¡ | dçt|ƒt|	ƒ › dè�¡ | dé¡ dêdë„ }Ft$ƒ }Gg }HdŸ|fdì|	ffD ]†\}}It%t&ƒ}J|ID ]0}K|J|K 	dí¡�pü|Kdî |F|Kƒd/kf  |K¡ �qä|dŸk�r$dïndð}L|J ¡ D ]"\\}M}N}O|H ||L|M|O|Nf¡ �q0�qÐdñD �]~\}P}Q‰ t‡ fdòd	„|HD ƒdódy„ dz�}R|R�s��q\t|Rƒ}S| dôt|Pƒ› d³|S› dš|Q› |Sdk�rÀdõnd…› dö�¡ |RD �]\}}T}M}O}Utd÷d	„ |OD ƒƒ}Vtdød	„ |OD ƒƒ}Wdùdú„ |OD ƒ}X|X�r$tt|Xƒƒnd¯}Yt'|Mƒ\}Z}[|[�rL|Z› dš|[› � (¡ n|M}\t|Oƒ}]|U�r€dû}^|]› dü|]dk�rvdõnd…› �}_n:|]dýk�rŽdþndÿ|]› �d �}^|]› �d|]dk�r²dõnd…› �}_|U�rÄd…n
|G 	|Md…¡}`|`�rè�dt|`ƒ› di�nd…}a| �d|T› d|› di|a› dpt|\ƒ› �d|^› �d|_› dpt|Vƒ› �d|Y› �d|Wd¹›dt�¡ |OD ]Ž}K|K�d d u�rht|K�d ƒnd¯}b|K 	�d	¡�p€|Kdî }c| �d
t|cƒ› �dt|K�d ƒ› dkt|K�d ƒ› dk|b› �d|K�d d¹›dt�¡ �qF�qÐ�q\|�sö|	�sö| �d¡ | du¡ |�r | �d¡ | �d¡ d™}d|D ]È}e|e 	d;¡d u �rd| �dt|e 	�d¡�pTd…ƒ› dt�¡ �q&|d|ed. 7 }d|ed; d/k�r„�dn�d|ed; › �}f| �dt|fƒ› �d|ed; › �dt|ed. ƒ› dk|e�d › dk|e�d › �d|ed] › dt�¡ �q&| �dt|dƒ› �d�¡ | du¡ | �d¡ |�rd| �dt|ƒ› �dt|ƒ› d�t|| d �d ƒ› �d �¡ �d!D ]@}g||g �d �rj| �d"|g› �d#t||g �d ƒ› dV�¡ �qj| d‚¡ | �d$¡ | �d%¡ |�d �rø| �d&t|�d ƒ› dt�¡ |d$  ¡ D ]ˆ\}f}|�s0| �d't|fƒ› �d(�¡ �q| �d't|fƒ› dk|d; › dk|d? dB›�d)|d@ dB›�d*t|d9 ƒ› �d|d= d>›dt�¡ �q| du¡ | �d+t| 	�d,¡�p²g ƒ› �d-�¡ | �d.¡ | �d/¡ |�d0 �r’| d‚¡ | �d1¡ | �d2¡ t| 	�d,¡�pg ƒ}hz*d/�d3l)m*}im+}j |ht|iƒt|jƒ O }hW n t,�yZ   Y n0 |�d0  ¡ D ]Œ\}Z}|�s|�qj|Z|hv }k|k�rŽdZn�d4}^| �d't|Zƒ› dk|^› dk|d; › dk|d? dB›�d)|d@ dB›�d*t|d9 ƒ› �d|d= d>›dt�¡ �qj| 	�d5¡�rb|�d5 }l| �d6|ld; › �d|ld? dB›�d)|ld@ dB›�d)t|ld9 ƒ› �d|ld= d>›dt�¡ | du¡ | �d7¡ | 	�d5¡�r’| �d8¡ | d‚¡ | 	�d9¡}m|m�r8t�d:dú„ |m�d;  ¡ D ƒ�d<dy„ dz�}nt�d=dú„ |m�d;  ¡ D ƒ�d>dy„ dz�}o�d$�d?�d@„}p| �dAt|nƒ› �dB�¡ | d_¡ | �dC¡ |nD ]J\}Z}| �d't|Zƒ› �dD|d; › dkt|d9 ƒ› �dE|d= d>›dt�	¡ �qB|oD ]L\}Z}| �d't|Zƒ› �dF|d; › �dt|d9 ƒ› �d|d= d>›dt�	¡ �q’|n�sø|o�sø| �dG¡ | du¡ | �dH|m�dI › �dJ|m�dK dQ›�dL�¡ | d_¡ | �dM¡ | |p�dN|m�dO ƒ¡ | |p�dP|m�dQ ƒ¡ | du¡ | d_¡ | �dR¡ | |p�dS|m�dT dI�dU�¡ | |p�dV|m�dW dI�dU�¡ | |p�dX|m�dY dI�dU�¡ | du¡ | �dZt|md ƒ› �d[�¡ | �d\¡ | �d]¡ | d‚¡ | 	�d^¡�pHi }qdŽ|q 	dŽ¡�pZi | 	d¡fg}rt�d_d	„ |rD ƒƒ}st�d`d	„ |rD ƒƒ}t| �da|s› �db�|t�r¶d*|t› �dc�nd… �dd ¡ | 	�d¡�rð| �det|�d ƒ› dV�¡ | d&¡ |rD �]8\}f}u}|u 	�df¡}|u 	�dgd/¡|u 	�dhd/¡ }v}wt|u 	�di¡�pFg ƒ}x|x�r^d*|x› �dc�nd…}y|�s†| dJ|f› �dj|y› d#�¡ �qþ|d9 dF |d;  t }|d/k�r¬dGn|d/k �rºdHnd…}z| dJ|f› �dk|z› d|v› �dl|w› d6t|ƒ› d*t|d9 ƒ› �dm|d? dS›dC|d@ dS›�dn|y› �dott|�p(dUƒƒ› d#�¡ �qþ| dV¡ �dpdú„ |rD ƒ}{|{�dqdú„ |rD ƒ7 }{|{j-�drdy„ �ds�dt� |{�rô| d_¡ | �du¡ |{D �]D\}f}|}}|| 	�dv¡}~|~�rÊ|~ �dw¡nd¸}|| 	d½¡�pÜdU› �dx|| 	d¾¡�pðdU› �}>|| 	�dy¡}€|€�r|€�d dS›�dz�nd¸}�|| 	�d{¡}‚|‚d u�rJ|‚�d dB›�d|�nd¸}ƒ|}�rZd¬}1n||�d} �rn�d~}1n�d}1| �d€t|ƒ› dkt|fƒ› �d�t|| 	�d‚¡�p¤d…ƒ› dkt|>ƒ› dkt|| 	�dƒ¡�pÆd…ƒ› dp|�› dq|ƒ› dk|1› dt�¡ �q | du¡ n| �d„¡ | �d…¡ | �d†¡ | d‚¡ �d‡d/i}„|„�d‡ �(r dF|„�dˆ  |„�d‡  }…| �d‰|„�d‡ › �dŠ�¡ |„ 	�d‹¡|„ 	�dŒ¡ }†}‡|†d u�r@|‡d u�r@|‡|†k}ˆ| d&¡ | �d�|ˆ�r¾dGndH› d|‡dS›�dŽ|„�d� d; › �d�|„�d‘ › �d’�	¡ | �d“|†dS›�dŽ|‡|† �d”›�d•�¡ |„ 	�dS¡}l|l�rœ|l�d– |†k}‰| �d—|„�d˜ �d™›�dš|‰�r`dGndH› d|l�d– dS›�dŽ|l�d– |† �d”›�d›|ld; › d#�¡ | �dœ|„�d� d/k�r¸dGndH› d|„�d� d>›�dž�|l�rî�dŸ|l�d� d>›�d �n�d¡ dV ¡ | dV¡ |†�r&t.�dt/|‡|† �d ƒƒnd/}Š| �d¢|Š› �d£�¡ | d_¡ | �d¤¡ �d¥D ]®\}‹}Œ|„ 	|‹¡}|�sx�q\|�d¦ d/k�rŒdGn|�d¦ d/k �r dHndI}z| �d§|Œ› dk|d; › �d��d|d;  |„�d‘  dS›�d¨|z› d|�d¦ d>›�d©|�dª dB›�d«�¡ �q\|„ 	�d¬d/¡|„ 	�d‘d/¡ }�|�d/k�rH| �d­|�› �d®�¡ | du¡ |„ 	�d¯¡�r|| �d°|„�d¯ › �d±�¡ | �d²¡ t�d³d	„ |„ 	�d´i ¡ #¡ D ƒ�dµdy„ �ds�dt�}Ž|Žd t0… }�|Ž� ràt|Žƒt|�ƒ }�|��rút|�ƒ› �d�t|Žƒ› �nt|Žƒ› }| �d¶|› �dd�¡ | d_¡ | �d·¡ |�D �]œ}|||�d¸ d/k�rNdGn||�d¸ d/k �rbdHndI}‘||�d¹ d u �rzd¸n||�d¹ dQ›�dz�}’||�dº d u �r¤d¯}“nV|„�d˜ d u�oÆ||�dº |„�d˜ k}”|”�rä�d»||�dº d’›di�n�d¼||�dº d’›di�}“||d; � r||d; › �d½�nd¸}•||d; dýk� r*dGn||d; � r8dHndI}–| �d€t||d» dXd … ƒ› dkt||d¼ ƒ› dkt||dÉ ƒ› �d¾|–› d|•› dlt||�d¿ ƒ› dk|“› �d�|’› dlt||�dÀ ƒ› �d¾|‘› d||�d¸ dQ›�d«�¡ �q4| �dÁ¡ | �dÂ¡ t1ƒ }—|—�$rø|—d$ }I|— 	�dÃ¡�!pg }˜|—�dÄ d u�!o.|—�dÄ dXk}|˜�!rJ�dÅt|˜ƒ› �dÆ�nd…}™| �dÇ|—�dÈ › �d�t|Iƒ› �dÉ|™› �dd�¡ | d_¡ | �dÊ¡ |I�!s¶| �dË|—�dÌ › �dÍ�¡ t&|Iƒt|˜�dÎdy„ dz� D �]x}et2|e 	�dÏ¡ƒ}ˆ|—�d˜ |e�dÐ  }š|ˆ�"r�dÑn$�dÒ|š�dÓk �"r|šdB›n|šd’› di }›då �dÔd	„ |e 	�dÕ¡�"pHg D ƒ¡}œ|e 	�dÖ¡�dÃk�"r˜|e 	�d×¡}��dØ|��"rŒ�dÙ|�dS›�dÚ�nd… di }žn4�dÛ�dÜdd�dÝœ 	|e 	�dÖ¡tt|e 	�dÖd…¡ƒƒ¡}ž| �d€|e�dÞ dB›�dßt|e 	d¼d…¡ƒ› dkt|e 	dÉd…¡ƒ› �d¾|ˆ�#rdGndI› dt/|e�dÐ ƒ�d™›�d�|œ› dk|›› dÒ|ž› dt�¡ �!qÐ| du¡ |—�dà |—�dá  }Ÿ|Ÿ�#rŒdF|—�dà  |Ÿ dS›�d|�nd¸} |�#r®�dâ|—�dÄ dS›�dã�n,|—�dÄ d u�#rÖ�dä|—�dÄ dS›�dÚ�n�då}¡| �dæ|—�d˜ �d™›�dçtt|—�dÀ ƒƒ› �dè|¡› �dé|—�dê › �dë|—�dà › �dì|—�dá › �dí| › �dî�|—�dï �$r~�dð|—�dñ › �dò|—�dñ |—�dó  › �nd… dV ¡ | �dô¡ |˜�$rî|— 	�dõ¡}¢| �dö|—�dÌ › �d÷�|¢�$rÞ�dø|¢�dù dS›�dú�n�dû �dü ¡ | d‚¡ |„ 	�dý¡�%pg }£|£�&r | �dþ|„�d˜ › �dÿ�¡ | d_¡ | �d ¡ t3�dd	„ |£D ƒƒ}¤|£D �]"}|�d d/k�%rtdGn|�d d/k �%rˆdHndI}z|„�d˜ d u�%o®|�d |„�d˜ k}¥|¥�%r¼�dnd…}^|�d |¤k�%rÖ�dnd…}¦|�d d/k�%rð�dn�d|�d �d™›�}Œ| �d§|Œ› |^› dk|�d‡ › �d�|�dˆ › dk|�d– dS›�d¨|z› d|�d¸ �d”›�d|z› d|�d d>›�dz|¦› dt�¡ �%qZ| du¡ | �d	¡ | d‚¡ �d
�d�d�d�dœ}§|„ 	�d¡�&pÈi }¨|„ 	�d¡}ktt|§ƒt|¨ƒB |k�&rò|khntƒ B ƒ}©|©�(rŠ| �dt|k�'pdUƒ› �dd�¡ | d_¡ | �d¡ |©D �]*}l|¨ 	|l¡�'pZd/d/d™�dœ}|�d �'r||�d¸ |�d  nd/}ª|ªd/k�'rŽdGn|ªd/k �'rœdHndI}z|l|kk�'r°�dnd…}^|�d �'rø�d|z› d|�d¸ dQ›�d�}«�d|z› d|ªd>›�d�}¬n�d}«�d}¬| �dt|lƒ› d²|^› dk|�d‡ �(p*d¸› �d�|�d �(p>d¸› �d|«› |¬› �d|§ 	|l�d¡› dt�¡ �'q<| du¡ | �d¡ | d‚¡ | �d¡ | d‚¡ | d‚¡ | �d t4› �d!�¡ | �d"¡ �d# |¡S (%  Nr   rÙ   r‚   ©r‚   rM   rK   rN   c                 s   s   | ]}|d  V  qdS ©r]   Nr   r  r   r   r   rß   Ÿ  r,   zbuild.<locals>.<genexpr>c                 s   s"   | ]}|d  dur|d  V  qdS ©r^   Nr   r  r   r   r   rß      r,   c                 s   s"   | ]}|d  dur|d V  qdS )r^   Nr]   r   r  r   r   r   rß   ¡  r,   z%<!doctype html><html lang="en"><head>z<meta charset="utf-8">zD<meta name="viewport" content="width=device-width, initial-scale=1">z/<meta name="color-scheme" content="dark light">z"<title>Soccer xlow tracker</title>z<style>z</style>z</head><body>z<div class="topbar"><div>z<h1>Soccer xlow tracker</h1>z7<div class="sub">snapshot <span id="built" data-built="Zseconds)Ztimespecrs  ú%Y-%m-%d %H:%Mu?   </span><span id="age" class="age"></span> Â· frozen rule since rÓ   u#    Â· caps $1/order both venues</div>aˆ  <script>(function(){var e=document.getElementById("built"),a=document.getElementById("age");if(!e||!a)return;function tick(){ var b=new Date(e.getAttribute("data-built")),m=Math.floor((Date.now()-b)/60000); if(isNaN(m))return; a.textContent=" ("+(m<1?"just now":m<60?m+"m old":(m/60).toFixed(1)+"h old")+")"; a.className="age"+(m>65?" stale":"");}tick();setInterval(tick,30000);})();</script>z</div><div class="tools">ur   <input class="filter" id="filterBox" type="search" placeholder="Filter league / teamâ€¦" aria-label="Filter rows">z9<button class="tool" id="expandBtn">Collapse all</button>z1<button class="tool" id="themeBtn">Light</button>z</div></div>rƒ   rÂ   z<div class="kpis">r/   zm<div class="kpi"><div class="lbl">Kalshi total</div><div class="val"><span class="dim">err</span></div></div>rP   u    Â· c                 s   s2   | ]*\}}|d krdn
d|› d�t |ƒ V  qdS )r   zprimary Úsubr  N)rì  )rT   rÞ   r¯   r   r   r   rß   Ó  s   ÿzE<div class="kpi"><div class="lbl">Kalshi total</div><div class="val">z</div><div class="lbl">cash r0   r   u    Â· pending rQ   z</div><div class="lbl dim">zn<div class="kpi"><div class="lbl">Poly-US total</div><div class="val"><span class="dim">err</span></div></div>zF<div class="kpi"><div class="lbl">Poly-US total</div><div class="val">z@<div class="kpi"><div class="lbl">At risk</div><div class="val">z</div><div class="lbl">z open</div></div>zD<div class="kpi"><div class="lbl">Fwd ROI/bet</div><div class="val">Úroiz</div><div class="lbl">n=r   u    Â· z=Úzú+.2fÚwrÚiwrzF<div class="kpi"><div class="lbl">WR vs implied</div><div class="val">ru  ú% vs z%</div></div>z
xlow paperrO   rÞ  rß  rà  z"<div class="kpi"><div class="lbl">z; <span class="dim">(not real)</span></div><div class="val">z0</div><div class="lbl">bankroll <b><span class="ré  z,.2fz$</span></b> <span class="dim">from $z </span></div><div class="lbl">n=ú+.1fu   % Â· WR rw  z+% implied</div><div class="lbl dim">frozen rµ   ry  r   é   z<span class="bad">STALE</span>z<span class="good">live</span>z.<details class="sec" open><summary>Live maker z <span class="count">(r3   z resting)</span></summary>r  z­<tr><th>Last post</th><th>Resting</th><th class="opt">Placed 1h</th><th>Legs posted</th><th>Filled</th><th>Strand rate</th><th class="opt">Exposure</th><th>Balance</th></tr>Úneverzm agor¥  z <span class="dim">&mdash;</span>z%<span class="clvsub">r¦  ú/r§  rë  z<tr><td class="ú	</td><td>z</td><td class="dim opt">r¡  r£  r¤  z</td><td><b>z</b></td><td class="dim opt">rŸ  z ct</td><td>$r€  r�  r¢  c                 s   s    | ]\}}|› d |› �V  qdS )z &times;Nr   )rT   Úkr  r   r   r   rß   4  r,   c                 S   s
   | d  S rÖ   r   rÌ   r   r   r   r+   5  r,   zbuild.<locals>.<lambda>r-   z,<div class="note">rejects in the last hour: u¥    â€” <b>post only cross</b> is the guard working: the book moved between reading the ask and sending, so the exchange refused an order that would have crossed.</div>z<div class="note">open: c                 s   s6   | ].}t |d   dd¡d ƒ› d|d d›�V  qdS )rS   rW   r   rX   r  r\   ú+.0fN)r�  rh   )rT   r   r   r   r   rß   ;  s   ÿa®  <div class="note"><b>Strand rate</b>, not fill rate: books that took at least one leg and did NOT complete, over books that took at least one leg. A book nobody touched is excluded &mdash; it cost nothing, and counting it only dilutes the number. A strand costs about <b>&minus;$0.69</b> against <b>+$0.09</b> for a clean book, so break-even sits near <b>12%</b> and the lifetime figure is 14%. Scoped to books opened on or after z¾, when the current maker (MIN_OPEN_H=6, 5 books) took effect &mdash; earlier books measure a maker that no longer exists. The n beside the rate is the whole caveat at this sample size.</div>z€<div class="note"><b>89%</b> optimistic ceiling. Kill switch: create the <code>STOP_MAKER</code> file in the project root.</div>rƒ  u    <span class="dim">Â· ú out-of-scope hidden</span>rb   rô   c                 3   s0   | ](}|d  t ˆ  |d ¡pdƒv rdV  qdS )r}   r   r   r   N)r’   r*   ©rT   rÞ   )Ú_zmapr   r   rß   V  s   ÿz z-gate</span>uE   <details class="sec" open><summary>Signals â€” today &amp; tomorrow (r  z
</summary>rú   rù   rî   u   xlow paper equity â€” frozen rt  z bets, $rî  z/bet))u   primary â€” live ruleu)   z-gate shadow â€” in-sample, not evidence)r  r	  z.<div class="card"><table data-defaultsort="0">z´<tr><th>Date</th><th class="opt2">Lg</th><th>Match</th><th>Ask / CLV</th><th class="opt">Fair</th><th class="opt2">CLV other</th><th class="opt">Real trade</th><th>Result</th></tr>z5<tr><td colspan="12" class="dim">no signals</td></tr>r    r  c                 s   s,   | ]$}d |dkrdnd› d|› d�V  qdS )z<span class="pill r˜   r!  r   rs  rë  Nr   ©rT   r*  r   r   r   rß   }  s   ÿr¹   z<span class="dim">n/a</span>r—   r˜   r™   rš   ÚKÚP)r˜   r³   rz   r{   r£   z.4fr¤   zl<span class="prov" title="result read from the match record; the ledger settles it at 04:00">&middot;</span>r|   z<span class="dim">open</span>r¶   z <span class="pill">rÝ  z<b>r  z</b>z <span class="dim">(rx  r·   r¸   r­   r  z.3frø   r   r}   r›   rœ   z <span class="dim kotime">z%H:%Mr   zª <span class="pill z" title="z-gate shadow: this league is below the floor, so the shadow rule would sit this one out. Not applied -- the bet still counts.">z-gate</span>r�   rž   z<span class="pick">z</span> vs r    z vs <span class="pick">r~   z<trz class=zdropz><td class="dim" data-sort="z</td><td class="opt2">z(</td><td class="mcell"><span class="mw">z</span></td><td data-sort="z<span class="clvsub">z </span></td><td class="dim opt">z</td><td class="opt">z</td><td data-sort="z &middot; WR <b>z%</b> (zW)zW) vs z% implied <span class="z	pp</span>u_   <tr class="nosort"><td class="dim"></td><td class="opt2"></td><td class="dim"><b>Total</b> â€” z settled of zY</td><td></td><td class="opt"></td><td class="opt2"></td><td class="opt"></td><td class="z"><b>ú</b></td></tr>zA<div class="note"><b>Result</b> is scored at the paper notional (z¼/bet), not at whatever the ledger happened to stake &mdash; so it matches the equity curve above and the paper KPI tile exactly. A win pays (1&minus;ask)/ask units, a loss costs one.</div>uT  <div class="note"><b>Ask</b> = the quoted price on each venue at signal time; the named (cheaper) venue is bold â€” that is what the bet costs. <b>Fair</b> = devigged Pinnacle probability. Two signals at the same CLV are very different bets at 0.14 and 0.47, and price is what drives the variance that makes this rule slow to measure.</div>c                 S   s   h | ]}|D ]}|’qqS r   r   )rT   r  r§   r   r   r   rV   	  s   ÿzbuild.<locals>.<setcomp>a  <div class="note"><span class="pill z">z-gate</span> marks a signal in a league the <b>shadow</b> exclusion rule currently sits out &mdash; below the floor on a point-in-time read of its own history, judged on results known BEFORE that row's date. Currently: <b>r‚  ak  </b>.<br>These rows are <b>not removed and not altered</b>. The signal fired, the bet is scored, and it still counts in the frozen forward test exactly as before &mdash; the mark only says what a rule that is not switched on would have done. Hiding them would put this table in disagreement with the number the test is accruing. See the League z-gate panel.</div>z3<details class="sec" open><summary>Open positions (z$)</summary><div class="card"><table>zZ<tr><th>Venue</th><th>Market</th><th>Qty</th><th>Cost</th><th>Value</th><th>Fees</th></tr>c              	   S   s.   zt |  d¡ƒW S  ttfy(   Y dS 0 d S )Nr\   r    )r"   r*   r#   r$   r%   r   r   r   Ú_qf9	  s    zbuild.<locals>._qfr²   rZ   rY   r!  r   ))zmarket makerZbookF)rî   r�   Tc                 3   s   | ]}|d  ˆ kr|V  qdS )r«   Nr   r&  )Ú
_want_xlowr   r   rß   e	  r,   c                 S   s   | d dk| d fS )Nr   r˜   r  r   ©r*  r   r   r   r+   f	  r,   z,<tr class="nosort grouphdr"><td colspan="6">rÞ   z)</span></td></tr>c                 s   s   | ]}|d  V  qdS r  r   ©rT   ré   r   r   r   rß   o	  r,   c                 s   s   | ]}|d  V  qdS )r_   Nr   r-  r   r   r   rß   p	  r,   c                 S   s    g | ]}|d  dur|d  ‘qS r  r   r-  r   r   r   r‘   q	  r,   zbuild.<locals>.<listcomp>z<span class="pill">xlow</span>z betr  z"<span class="good">complete</span>z<span class="bad">z of 3</span>z legz+<br><span class="pill" title="pricing arm">z<tr><td><span class="pill z</b> ú</td><td class="dim">z</b></td><td>z</td><td class="dim">$r^   r[   uJ   <tr class="nosort"><td></td><td class="dim" style="padding-left:18px">â†³ r\   r]   r_   z3<tr><td colspan="6" class="dim">none open</td></tr>z1<div class="card" style="margin-top:10px"><table>zT<tr><th>Subaccount</th><th>Cash</th><th>Long</th><th>Short</th><th>Resting</th></tr>z8<tr><td colspan="5" class="bad">subaccounts unreadable: r   rÿ   zsubaccount z<tr><td><b>z</b> <span class="dim">#z</span></td><td>r1   r2   z9<tr class="nosort"><td class="dim">total cash</td><td><b>zE</b></td><td colspan="3" class="dim">across all subaccounts</td></tr>z±<div class="note">The Kalshi app shows the <b>primary</b> subaccount only, so anything held outside it is invisible there &mdash; this table is the only place it shows up.</div>z'<div class="note">Poly-US marked value z	 vs cost r  uG   ) â€” note thin books mark to a low bid, which overstates losses.</div>rL   z<div class="note bad">z API error: zb<details class="sec" open><summary>Forward test (out-of-sample)</summary><div class="card"><table>zU<tr><th>Rule</th><th>n</th><th>WR</th><th>Implied</th><th>ROI/bet</th><th>z</th></tr>z <tr><td colspan="6" class="bad">z<tr><td>u.   </td><td colspan="5" class="dim">â€”</td></tr>z%</td><td class="dim">ú
%</td><td>z3<div class="note"><b>Scope.</b> The primary row is r…   a{   leagues: the 20 the backtest was measured on &mdash; a universe fixed before this test started &mdash; <b>minus BSA and CL1</b>, plus NWSL. Leagues outside it are still scored, separately: the pooled and out-of-scope summary rows are no longer shown here, but both series are still written daily to <code>forward_test_unique_book_log.csv</code>, and every out-of-scope league still appears in the by-league table below.<br><b>PL1 and USL removed 2026-08-27.</b> Neither has any Polymarket.com market, so the rule could never fire on them directly &mdash; they entered only through a <b>Poly-US</b> substitution the n=286 backtest never used, and that slice ran flat (+0.8% on n=71) while the Poly.com-confirmed legs ran +34.5%. They keep scoring in the by-league table, the daily CSV and the poly-US row, so nothing stops being measured; it just stops counting toward the frozen rule.</div>aV  <div class="note bad"><b>Read the primary with care.</b> BSA and CL1 were removed on 2026-08-23 <i>after</i> they showed as the two worst leagues in the scope (&minus;40.2% on n=35 and &minus;75.2% on n=13). That is post-hoc selection, unlike the original backtest scope, which was fixed in advance. It moves the headline from +18.75% (z+1.37) to +34.46% (z+2.55) and nearly all of that is the two removals &mdash; so the z crossing 1.96 is <b>manufactured by the choice, not earned by the rule</b>. Everything before 2026-08-23 is in-sample for this scope; only bets struck after it test it.</div>z¼<div class="note">unique/overlap are diagnostics, not traded: the rule bets "unique" shapes and skips "overlap" ones, so a wide spread between them means the gate is doing real work.</div>rò   zW<details class="sec" open><summary>Forward by league</summary><div class="card"><table>za<tr><th>Lg</th><th>Scope</th><th>n</th><th>WR</th><th>Implied</th><th>ROI/bet</th><th>z</th></tr>r†   z<span class="dim">track</span>rö   zi<tr class="nosort"><td class="dim">all poly-US confirm *</td><td class="dim">&mdash;</td><td class="dim">u�   <div class="note">at these sample sizes every cell is within ~1 game of its implied count â€” league-level reads are noise, not signal.</div>u7  <div class="note"><b>Scope</b> column: <span class="good">live</span> = armed for real money at $1; <span class="dim">track</span> = signals and forward test only, no orders.<br>* <b>poly-US confirm</b> = USL, PL1 and CH1, which have no Polymarket.com market at all, so the same strict rule uses Polymarket-US as the confirming book. Those leagues are listed above like any other and their total is repeated here so the mix can be decomposed. The n=286 +24.7% backtest used Poly.com as the confirming book â€” whether Poly-US confirms Pinnacle as well is <b>untested</b>.<br>Of those three only <b>CH1</b> is armed (2026-09-05): it is the one with a verified Poly-US market, and it trades WITHOUT being in the frozen primary, so its bets stay in the poly-US row and never move the headline. USL and PL1 stay on paper.</div>ró   c                 S   s    g | ]\}}|d  r||f‘qS )rÜ   r   rÝ   r   r   r   r‘   G
  r,   rÍ   c                 S   s   | d d S ©Nr   r  r   rÌ   r   r   r   r+   H
  r,   c                 S   s*   g | ]"\}}|d  s|  d¡s||f‘qS )rÜ   Zwarmr)   rÝ   r   r   r   r‘   I
  s   ÿc                 S   s   | d d S r0  r   rÌ   r   r   r   r+   K
  r,   c                 S   s\   |sd|› d| › d�S d|› d| › d|d › d|d d›dt |d	 ƒ› d
|d d›d|› �S )Nz<tr class="z"><td>z></td><td colspan="4" class="dim">nothing settled yet</td></tr>r   r   r  ru  r/  r  r.  r  r  r€  )rç  )r‘  rÞ   ræ  Znoter   r   r   Ú_zrowM
  s    ÿ
ÿþþzbuild.<locals>._zrowuK   <details class="sec"><summary>League z-gate <span class="count">(shadow Â· z dropped)</span></summary>zF<tr><th>Lg</th><th>State</th><th>n</th><th>ROI/bet</th><th>z</th></tr>z2</td><td><span class="bad">dropped</span></td><td>z</td><td class="bad">z9</td><td><span class="dim">ok</span></td><td class="dim">zC<tr><td colspan="5" class="dim">no league past warmup yet</td></tr>z9<div class="note">A league is <b>dropped</b> once it has rÔ   z%+ settled bets and its z falls below rÕ   a¦  . State is point-in-time: a dropped league that recovers above the floor comes back on its own, and the bets it lost while out are not replayed.<br><b>Primary scope only.</b> The gate runs inside the frozen scope, so the track-only leagues listed in the table above (BSA, CO1, JP1, CN1 &hellip;) are absent here by design &mdash; several are far worse than anything on this list and are already outside the headline.</div>zM<tr><th>Since freeze</th><th>n</th><th>WR</th><th>ROI/bet</th><th>z</th></tr>zgated (shadow)rÑ   zungated (control)rÒ   zœ<tr><th class="dim">Before freeze &mdash; in-sample</th><th class="dim">n</th><th class="dim">WR</th><th class="dim">ROI/bet</th><th class="dim">z</th></tr>r:  rÎ   )ræ  ZungatedrÏ   zthe dropped betsrÐ   a_  <div class="note bad"><b>Discovery, not validation.</b> This gate was found by reading the by-league table above &mdash; the same sample the "before freeze" rows score &mdash; so those rows are <b>guaranteed</b> to flatter it and are not evidence of anything. That is the identical error to the BSA/CL1 scope edit, one level deeper. Only the <b>since z.</b> rows test it, and they start empty.</div>a„  <div class="note"><b>Why exclude and not select.</b> The question asked was the opposite one &mdash; bet only leagues with z above a threshold. Walk-forward that fails: inside the primary scope it returned +8.9% on the bets it took against +17.3% on the bets it skipped, and a random draw of the same size beat it two thirds of the time. Selecting winners needs the top tail to persist and costs ~80% of the sample; excluding losers needs only the bottom tail and costs ~5%. League-level persistence is real but weak (first-half z vs second-half ROI r=+0.35, p=0.03 over 29 leagues) &mdash; enough to find the worst, not to rank the best.</div>a  <div class="note"><b>Shadow only.</b> Nothing here gates an order or changes the frozen primary, which stays unfiltered above. Both series are written daily to <code>forward_test_unique_book_log.csv</code> as <code>xlow_zgate_*</code> and its control <code>xlow_postz_*</code>.</div>r÷   c                 s   s&   | ]\}}}t | d ¡pg ƒV  qdS )rç   NrL  ©rT   r  r¶  r   r   r   rß   ¨
  r,   c                 s   s&   | ]\}}}t | d ¡pg ƒV  qdS )rq   NrL  r2  r   r   r   rß   ©
  r,   uJ   <details class="sec" open><summary>xlow â€” last 24h <span class="count">(z settledz openz)</span></summary>z/<div class="note bad">forward test unreadable: rÇ   rè   ré   rq   uj    <span class="dim">(24h, paper)</span></div><div class="val dim">â€”</div><div class="lbl">no settled betsz< <span class="dim">(24h, paper)</span></div><div class="val z&ndash;z/bet</div><div class="lbl">WR z	% impliedz"</div><div class="lbl dim">frozen c                 S   s0   g | ](\}}}|  d ¡pg D ]}||df‘qqS )rç   Fr)   ©rT   ra   r¶  r  rG   r   r   r   r‘   Ó
  s   ÿc                 S   s0   g | ](\}}}|  d ¡pg D ]}||df‘qqS )rq   Tr)   r3  r   r   r   r‘   Õ
  s   ÿc                 S   s   | d   d¡ptjS )Nr   rá   )r*   r   rA  r,  r   r   r   r+   ×
  r,   TrÈ  z‰<tr><th>KO</th><th>Rule</th><th class="opt2">Lg</th><th>Match</th><th>Side</th><th>Price</th><th class="opt">CLV</th><th>Result</th></tr>rá   ú%m-%d %H:%Mz v r°   r¯   Úclvú%rª   z<span class="good">W</span>z<span class="bad">L</span>z<tr><td class="dim">z</td><td class="dim opt2">rÅ   r  zs<div class="card"><div class="note">xlow did not fire on a fixture that kicked off in the last 24 hours</div></div>a  <div class="note"><b>All leagues, unlike the headline.</b> This panel is a recent-activity view, so it covers everything being tracked &mdash; including the leagues added for collection, which the primary forward number excludes. It will not tie out to the Fwd ROI tile.</div>a  <div class="note"><b>Paper.</b> The real xlow ledger is the signals panel above. Prices are the ask the rule would have taken, so these are pre-fee.<br>A day is <b>noise</b> at this per-bet variance &mdash; the frozen decision point is n=1000, not a good afternoon.</div>r  r  zE<details class="sec" open><summary>Market maker <span class="count">(z paper books)</span></summary>r6  r8  zB<div class="kpi"><div class="lbl">Completion</div><div class="val z%</div><div class="lbl">r4  z of r7  z books with a fill</div></div>zC<div class="kpi"><div class="lbl">Break-even</div><div class="val">r"  zpp margin</div></div>r+  z3<div class="kpi"><div class="lbl">At live gate &ge;r	  r  z</div><div class="val u   pp Â· n=zI<div class="kpi"><div class="lbl">Modelled per book</div><div class="val r9  zc</div>z<div class="lbl">zc at the gate</div>z.<div class="lbl">from the two rows below</div>zK<div class="note">completion vs break-even<div class="bar"><i style="width:z%"></i></div></div>zg<tr><th>Outcome</th><th>Books</th><th class="opt2">Share</th><th>Per book</th><th>Spread (SD)</th></tr>))r4  u   complete â€” all 3 legs sold)r5  u   partial â€” stranded on 1 or 2r1  z<tr class="nosort"><td>z%</td><td class="zc</td><td class="dim">&plusmn;r2  zc</td></tr>r.  uQ   <tr class="nosort"><td class="dim">no fill â€” never at risk</td><td class="dim">uY   </td><td class="dim opt2">â€”</td><td class="dim">0.00c</td><td class="dim">â€”</td></tr>r/  r~  uB   older books are excluded from this panel: they predate the volume column, so the gate cannot be evaluated against them. Same rule as the sweep below. They are all v1/v2, whose quoting rule sat above the market&rsquo;s own ask on 86% of legs â€” pooling them in moves break-even from 61% to 79% on retired behaviour.</div>ux  <div class="note">Read the <b>Spread</b> column, not the P&amp;L. A complete book collects the overround whoever wins, so it lands in a narrow band; a partial book is a naked position on one outcome and swings ~25&times; wider for a mean that is not distinguishable from zero. That is why completion rate is the headline and total P&amp;L is not: the profit is a near-certainty and all the variance is partial fills. Modelled per-book above is completion&times;complete + (1&minus;completion)&times;partial â€” both measured far more precisely than realised P&amp;L, which would need ~841 books at the current gate to resolve.</div>c                 s   s   | ]}|D ]
}|V  q
qd S r   r   rØ   r   r   r   rß   _  r,   r!  c                 S   s   | d | d  fS )Nr   r   r   rã   r   r   r   r+   `  r,   z@<details class="sub"><summary>Recent books <span class="count">(z·<tr><th>Date</th><th>Lg</th><th>Match</th><th>Filled</th><th class="opt">Legs</th><th>Min leg vol</th><th class="opt2">Gross @post</th><th class="opt">Regime</th><th>P&amp;L</th></tr>r  r  r   z<span class="good">z<span class="dim">z/3z</td><td class="r  r  z</table></div></details>aµ  <div class="note" style="border-left:3px solid var(--warn);padding-left:9px"><b>These are CEILING numbers, not results.</b> Fills here come from mm_settle, which counts any trade printed at or above our ask as ours &mdash; it ignores queue position, and its own docstring calls it a ceiling. The live quoter uses a stricter queue-aware test, and the two disagree: the settler scores 59 books as 2-of-3 while the quoter took no action on 39 of them, because it never believed two legs had sold. Completion rates and the P&amp;L split above are therefore upper bounds. The live view is now being archived to <code>mm_live_fills.csv</code> so the gap can be measured rather than guessed at.</div>rO  rS  ú
 &middot; z aheadzT<details class="sub" open><summary>Liquidity gate &mdash; live <span class="count">(rT  z books in window passz�<tr><th>To KO</th><th>Lg</th><th>Match</th><th>Min leg vol</th><th class="opt2">All three legs</th><th>Gate</th><th class="opt">Stage</th></tr>z0<tr><td colspan="7" class="dim">no books in the rP  zh window right now</td></tr>c                 S   s   |   dd¡S )NÚhrsr   r)   r%   r   r   r   r+   «  r,   rR  Zminvolz<span class="good">pass</span>z<span class="bad">&minus;r  c                 s   s   | ]}t |ƒd ›V  qdS )r  N)r<  rM  r   r   r   rß   ¶  r,   r  ÚstageZ	vol_age_mz!<span class="dim">ahead of windowz &middot; vol zm oldz <span class="good">quoted</span>z'<span class="dim">thin overround</span>)ZquotedZgross_rejectr	  r8  z
h</td><td>rU  rV  z<span class="bad">snapshot u#   m old â€” quoter may be down</span>z	snapshot zsnapshot age unknownz<div class="note">gate <b>&ge; z!</b> on the thinnest leg (regime u   ) Â· z	<br>last rÚ   z day(s): <b>z!</b> distinct books admitted, <b>u4   </b> turned away â€” the gate is letting through <b>z</b> of what it seesrü   u
    Â· today rW  z / rX  a!  <div class="note">The reject column shows how far <i>short</i> a book fell, so a gate set too high shows up as a column of near-misses rather than as nothing at all. Books are counted once per day, and one that later trades enough volume to clear the gate stops counting as rejected.</div>rQ  zö<div class="note">Rows marked <b>ahead of window</b> are not decisions. The gate has not ruled on them and they are not in the admitted/turned-away counts above &mdash; they are there so you can see what is coming rather than only what is in the z#h window. Their volume is refreshedz every r©  z minz on a slow sweepzÕ, not every cycle, because each one costs Kalshi calls. Volume only rises, so a book short of the gate this far out can still clear it by kickoff &mdash; read a <b>pass</b> here as a forecast, not a verdict.</div>r-  z1<details class="sub"><summary>Why the gate is at z7 <span class="count">(threshold sweep)</span></summary>z‰<tr><th>Min leg volume</th><th>Books kept</th><th class="opt2">Completed</th><th>Completion rate</th><th>Total</th><th>Per book</th></tr>c                 s   s   | ]}|d  V  qdS )r,  Nr   r$  r   r   r   rß   ÷  r,   r,  r*  z& <span class="pill k">live gate</span>u"    <span class="dim">â†� best</span>zno gatez&ge; zc</td><td class="uÂ  <div class="note">Each row asks: <i>if we had only quoted books whose thinnest leg had at least this much traded volume, what would have happened?</i> Completion improves monotonically up to roughly 500&ndash;1,000 and then <b>reverses</b> â€” the highest threshold has the best completion rate and still loses money, plausibly adverse selection, since the busiest markets are where an informed counterparty is most likely to be the one lifting us. Raising the gate is not free.<br><br>This is <b>in-sample</b>: the threshold was chosen from these same books. It is frozen from 2026-08-09 (regime v4) to be judged on fresh ones. Books with no volume recorded are excluded rather than counted as zero.</div>u³   flat <code>fair + 2c</code>, <b>uncapped</b> â€” sat above the market&rsquo;s own ask on 86% of legs, so it essentially could not fill. 3h window, fired once from a 5-minute cron.z°margin <b>staged</b> by hours-to-kickoff (6/5/3/2/1c), <b>capped</b> at the market&rsquo;s own ask and <b>floored</b> at fair. 12h window, persistent process polling every 20s.u5  <b>delayed completion</b> â€” only cross into the bid inside the last hour before kickoff, plus a 0&ndash;5c price ladder. Earlier regimes completed the instant a book hit 2-of-3, in practice 8&ndash;10 <i>hours</i> early, paying ~2c to cross while the third leg still had most of its window to sell for free.u€  v3 <b>plus the liquidity gate</b> â€” quote only books whose thinnest leg has traded volume &ge;500. Changes <i>which books are quoted</i>, not how they are priced, so it is a regime not a tweak: on 2026-08-09 it admitted 4 of 30 otherwise-playable books. Aimed at partial fills, which is where the losses live &mdash; a 2-of-3 book runs &minus;7.62c while a complete one runs +1.38c.)Zv1Zv2Zv3Zv4r#  r
  zO<details class="sub"><summary>What the regimes mean <span class="count">(live: zm<tr><th>Regime</th><th>Books</th><th class="opt2">Legs</th><th>P&amp;L</th><th>Per leg</th><th>Rule</th></tr>)r  r  r  r  z! <span class="pill k">live</span>z<td class="zc</td>u   <td class="dim">â€”</td>z)<td class="dim">no settled books yet</td>z<tr class="nosort"><td><b>z</td>z;<td style="white-space:normal;max-width:520px" class="dim">ú&mdash;uR  <div class="note">Regimes are <b>different strategies, not tweaks</b> â€” pooling them once produced a &ldquo;today&rsquo;s maker result&rdquo; that blended three configurations logged hours apart. Each book carries the regime stamped at post time, so a mixed day cannot silently average them. Compare within a regime, never across.</div>u  <div class="note">A <b>complete</b> book earns the overround and is outcome-independent; a <b>stranded</b> one is a naked position on whichever side the market is moving toward â€” which is why 2-of-3 is the worst state, not 1-of-3. The gate quotes only books liquid enough to finish: on this history, min leg volume &ge;500 lifted completion 36% &rarr; 60% and turned &minus;1.84c/book into +3.01c. That threshold was chosen on this same data, so it is <b>in-sample</b> â€” it is frozen from 2026-08-09 to be judged on fresh books only.</div>z<script>z	</script>ú</body></html>Ú
)rb   rb   )5r   rü   r   rk   rJ   r  r–   r„   rý   r*   rÙ  rd   ÚCSSrC   r   rþ   Ústrftimer@  rB   ri   rì  rA   rç  rò  ÚPAPER_BANKROLL_STARTr�  r@   r¹  r“  r­  r  r’   r¥   r¼  rè  re   ry   r   rf   rÀ  rj   r“   r‡   rˆ   r   r�   rA  r<  ÚMM_BOOKS_MAXr[  rÖ  r‡  ÚJS)­rü   Útomr  Zsubsr•   ÚsigsÚsigs_hiddenÚhealthÚk_posÚp_posÚat_riskZp_valZp_costÚhÚprimZ_kvZ_byZ_subtxtZ_pvZgap_ppr‘  rÞ   ZsinceZnetZbankZbank_clsr×  ZstaleZhdrZlpÚfrr�  Z_hidZ_znZ_zhdrZ_zcZnet_settledZ	n_settledZn_winsZsettled_asksr¹   Z_cheapZ_askZ_tagZsort_keyZ_settled_pnlZ_ledr{   Z_provÚresÚstratZask_txtZclv_txtZfair_txtZ_kmZs_koZko_txtr   Z_zpillr¨   r©   Z_sider~   Znclsr  ÚimpZwr_txtZedgeZeclsZ_zanyr*  Z_armsZ_allrƒ   Z_gr&   Z_pillrZ   Z_is_xlr  Z_sectZ_whatZ_selZ_nZpillZis_xlowZg_costZg_feesr÷  Zg_valr§   r¿  rc   r   ÚtagZunitZ_armZ	_arm_htmlÚvalr[   Z_totrx   ra   ZvnZ_scoper‡   rˆ   rv   r"  ZzgZ_droppedZ_okr1  Zl24Z_rulesZn_setZn_openr¶  rè   ré   Zn_opZopnræ  Zrows24rG   Zis_openrá   rÅ  r°   Zpxsr5  ZclvsZmkr+  ZbeZlrZokZgokr   r.   ZlblZnofillZallbooksZshown_booksZmoreZpcZgrZvolZpassesZflZfcZgsrO  Zaheadnr2   r	  r  Zvar9  rG  Zsharerµ  Úswr-  ZbestZis_liveZpeakZREG_DOCÚregsÚnamesr,  Zpnl_tdZper_tdr   )r+  r%  r   Úbuild’  s€   ÿþ$û

ÿþü

	







þ
ÿþýü

ÿþý
ÿ

ÿþþ
ÿþþ
ÿþ	÷	÷
öõõôôó

ÿÿ


ÿÿÿÿÿþýýüûú



ÿ
þþùù

ÿÿÿÿÿ
	ÿþû	




ÿ
ÿ

, 

þþÿÿ
ÿÿ(ÿ	$*ÿþþ
ýüüüûûúùø	÷
ö
ö 
 ÿþþþýýúú

ÿ

"üü

.ÿÿþþþ
ÿÿÿÿÿþýýü
ÿÿþýý
ü
ÿ
$ÿ
þýýüÿ
ÿ
.
*ÿ
ÿþ
ÿ


ÿÿÿþ
þý
	þýü
ûú


	ÿþÿ
ÿ
ÿþÿ
ÿþ
ÿ
þ




ú
ÿþþÿ

ý
 þþþý
ýüüûú

("ÿþýüûúù


ÿ
ÿÿþþÿ
þÿþ
ýüü
ÿ
þÿüû
$ÿ

,ÿþ
þ
ý
ÿ
ÿÿÿ

,($ÿ$
ÿ
ÿþþýüûúù
ùÿÿþ
ÿ ÿþÿ$ÿþþýÿþýüûúú
&ÿ
ÿ
(ýÿÿþþýüÿ.úúü"úù
ÿ

,$,ÿÿ
þýýü
üü

ô&ÿ

$ ÿþýýû



rT  z?padding:5px 8px;border-bottom:1px solid #ddd;white-space:nowrapz‚padding:5px 8px;border-bottom:2px solid #999;text-align:left;font-size:11px;text-transform:uppercase;color:#555;white-space:nowrapzDborder-collapse:collapse;width:100%;font-size:13px;margin-bottom:4pxc                 C   sD   | d u rdS | dkrdn| dk r$dnd}d|› d| d|› d	�›d
�S )Nú'<span style="color:#999">&mdash;</span>r   ú#1a7f37ú#c1272dz#555ú<span style="color:rs  râ  r®   rã  r   ©r  rå  r¯   r   r   r   Ú_e_pcto  s    rZ  c                 C   sD   | d u rdS | dkrdn| dk r$dnd}d|› d| d|› d	�›d
�S )NrU  r   rV  rW  z#000rX  ré  rê  r®   rë  r   rY  r   r   r   Ú_e_moneyv  s    r[  c               )   C   s  t  ¡ } | tdd� }tƒ }tƒ }tt|  ¡ | ¡ h| d¡d�|ƒ\}}t	ƒ }|d d |d d  }}t
dd	„ |D ƒƒt
dd	„ |D ƒƒ }	|d
  d¡}
dg}| d¡ | dt ¡  d¡› d|d › d�¡ |d d d uròt|d d ƒnd}|d d d u�rt|d d ƒnd}| d|› d|› dt|	ƒ› dt|ƒt|ƒ › d�	¡ |
�r²| d|
d › dt|
d ƒ› d|
d d ›d!|
d" d ›d#t|
d |
d"  ƒ› d$|
d% d&›�¡ | d'¡ | d(t|ƒ› d)�|�ràd*|› d+�nd, d- ¡ | d.t› d/�d, d0d	„ d1D ƒ¡ d2 ¡ |�s.| d3t› d4�¡ |D �]\}d5 |d6 ¡�pJd7}|d8 �r¢|d8 d9 }|d u�rŠt|d8 d: ƒ› d;t|ƒ› �nt|d8 d: �pœd<ƒ}nd=}|d> d?k�r¸d,nd@t|d> ƒ› dA�}| d3t› dBt|dC dDd … ƒ› dEt› dFt|dG ƒ› dEt› dFt|dH ƒ› |› dEt› dFt|dI ƒ› dEt› dBt|dJ �pBdKƒ› dEt› dFt|dL ƒ› dEt› dFt|dM ƒ› dEt› dF|› dEt› dF|› dN�&¡ �q2| dO¡ | dPt|ƒt|ƒ › dQ�¡ | d.t› d/�d, dRd	„ dSD ƒ¡ d2 ¡ dT|fdU|ffD ]”\}}|D ]„}|dV d u�rt|dV ƒndW}| d3t› dF|› dEt› dFt|dX ƒ› dEt› dBt|dY ƒ› dEt› dFt|dZ ƒ› dEt› dF|› dN�¡ �q �qô|�s¨|�s¨| d3t› d[�¡ | dO¡ | d\¡ | d.t› d/�d, d]d	„ d^D ƒ¡ d2 ¡ |d
  ¡ D ]Š\}}|�s�qò| d3t› dFt|ƒ› dEt› dF|d › dEt› dF|d d ›d_t› dB|d" d ›d_t› dFt|d ƒ› dEt› dB|d% d&›dN�¡ �qò| dO¡ g }|D ]Â}|d` �s¢�q�|da }g }|db �rÌ|d �sÌ| dc¡ |dd �rê|dd �sê| de¡ |df �r,|dg |df k �r,| |df |dg  › dh|df › di�¡ |�r�| |dj |dk dl |¡f¡ �q�| dmt|ƒ› dQ�¡ |�rô| d.t› d/�d, dnd	„ doD ƒ¡ d2 ¡ |D ]H\}}}| d3t› dFt|ƒ› dEt› dBt|ƒ› dEt› dpt|ƒ› dN�¡ �qž| dO¡ n
| dq¡ | dr¡ | ds¡ dt |¡S )uNr   rÙ   r‚   r  rM   rK   rN   c                 s   s   | ]}|d  V  qdS r  r   r  r   r   r   rß   ‡  r,   zbuild_email.<locals>.<genexpr>rƒ   rÂ   zU<html><body style="font-family:Arial,Helvetica,sans-serif;font-size:13px;color:#111">zB<h2 style="margin:0 0 2px;font-size:17px">Soccer xlow tracker</h2>ú:<div style="color:#666;font-size:12px;margin-bottom:12px">r  z &middot; frozen rÓ   z( &middot; $1/order cap both venues</div>r/   rž  zz<div style="background:#f4f6f8;border:1px solid #dde;padding:9px 11px;border-radius:6px;margin-bottom:14px"><b>Kalshi</b> z &nbsp;|&nbsp; <b>Poly-US</b> z &nbsp;|&nbsp; <b>At risk</b> rt  z open)z<br><b>Forward test</b> n=r   ú &nbsp; ROI r  z &nbsp; WR r  ru  z% vs implied r  ú% (z) &nbsp; z=r  r  ry  zS<h3 style="font-size:14px;margin:16px 0 6px">Signals &mdash; today &amp; tomorrow (r  z3 <span style="font-weight:400;opacity:.6">&middot; r#  rb   z</h3>z<table style="z"><tr>c                 s   s    | ]}d t › d|› d�V  qdS ©z<th style="rs  z</th>N©ÚE_THr|  r   r   r   rß   ¤  r,   )	ZDateÚLgÚMatchZBetZCheapzCLV KzCLV PZTradedZResultz</tr>z<tr><td style="z"" colspan="9">no signals</td></tr>r‚  r¹   z-<span style="color:#c1272d">not placed</span>rz   r{   r|   r7  rµ   z$<span style="color:#999">open</span>r¶   rî   z <i style="color:#777">z</i>z;color:#777">r   r  z</td><td style="rs  r}   r~   r�   r—   r:  r·   r¸   r€  ú</table>z=<h3 style="font-size:14px;margin:16px 0 6px">Open positions (z)</h3>c                 s   s    | ]}d t › d|› d�V  qdS r_  r`  r|  r   r   r   rß   À  r,   )ZVenueZMarketZQtyZCostZValuer˜   r²   r^   rU  rY   r\   r]   z!" colspan="5">none open</td></tr>zN<h3 style="font-size:14px;margin:16px 0 6px">Forward test (out-of-sample)</h3>c                 s   s    | ]}d t › d|› d�V  qdS r_  r`  r|  r   r   r   rß   Ò  r,   )ZRuler   ZWRZImpliedzROI/betr  z%</td><td style="rÔ  rv   rÒ  zno Kalshi pricesrÍ  zno Poly.com pricesr¿  rÓ  r  z teams missing abbrrÑ  ra   z; z<<h3 style="font-size:14px;margin:16px 0 6px">League issues (c                 s   s    | ]}d t › d|› d�V  qdS r_  r`  r|  r   r   r   rß   ñ  r,   )rb  ÚNameZIssuez;color:#c1272d">zI<div style="color:#1a7f37">all xlow-capable leagues have live feeds</div>z¦<div style="color:#888;font-size:11px;margin-top:16px">Generated by main/dashboard.py. Thin Poly-US books mark to a low bid, which overstates unrealised losses.</div>r;  r<  )r   rü   r   rk   r  r–   r„   rý   r*   rÙ  rd   rC   r   rþ   r>  r[  rA   rZ  ÚE_TBLr@  ÚE_INr�  ri   )rü   rB  r  r•   rC  rD  rE  rF  rG  rH  rJ  rI  ÚkbÚpbrÞ   r¹   r{   rL  rM  r‘  rƒ   r&   rP  ra   Zprobsrõ  ZmissingrÑ  Zissuer   r   r   Úbuild_email}  sš   ÿþ$
ÿÿ
$&þþýý

ÿþþýý
ÿÿýý
ÿÿý

ÿ"ÿ&"ÿ
ÿþ
þþý
ýüüû
ûú
úùùøø	

ÿÿýÿ
ÿþ
þý
ýüü


ÿÿý&ÿÿþþý
ýüü



&"ÿþÿÿþþ


rj  c                 C   sþ   ddl }ddlm} ddlm} ddlm} ddlm}m	}	m
}
 |rH|
sTtdƒ dS |d	ƒ}| |d
< ||d< |	|d< | ||dƒ¡ || d¡dd�}|jdd|d� | |¡ | dd¡�.}| ||
¡ | ||	| ¡ ¡ W d  ƒ n1 sð0    Y  dS )zÓEmail a short summary body with the full dashboard attached as .html.
    Rolled here rather than reusing daily_signals.send_email because that one
    is MIMEMultipart('alternative') with no attachment support.r   N)ÚMIMEMultipart)ÚMIMEText)ÚMIMEApplication)ÚEMAIL_ADDRESSÚEMAIL_RECIPIENTÚEMAIL_APP_PASSWORDz0  Email not configured (no .env). Skipping send.FZmixedZSubjectZFromZToÚhtmlr   )Z_subtypezContent-DispositionZ
attachment)Úfilenamezsmtp.gmail.comiÑ  T)ÚsmtplibZemail.mime.multipartrk  Zemail.mime.textrl  Zemail.mime.applicationrm  r“   rn  ro  rp  r   ZattachÚencodeZ
add_headerZSMTP_SSLZloginZsendmailZ	as_string)ZsubjectZ	body_htmlZattach_htmlrr  rs  rk  rl  rm  rn  ro  rp  ÚmsgZattZsrvr   r   r   Úsend_with_attachment  s(    
0rv  c               
      s  t ƒ } tƒ }ttt ¡  ¡ t ¡ tdd�  ¡ hƒ|ƒ\}}| d d | d d  }}tdd„ |D ƒƒtdd„ |D ƒƒ }|d  	d	¡}td
d„ |D ƒƒ}dg‰ ˆ  
d¡ ˆ  
dt ¡  d¡› d�¡ | d d duræt| d d ƒnd}	| d d du�rt| d d ƒnd}
ˆ  
d¡ ‡ fdd„}|d|	ƒ |d|
ƒ |dt|ƒ› dt|ƒt|ƒ › d�ƒ |dt|ƒ› |�r~d|› d�nd ƒ |�rò|dd |d! › d"t|d# ƒ› d$|d% d&›�ƒ |d'|d( d)›d*|d+ d)›d,t|d( |d+  ƒ› d-�ƒ ˆ  
d.¡ ˆ  
d/¡ ˆ  
d0¡ d1 ˆ ¡S )2zMShort at-a-glance body so the email is useful without opening the attachment.r   rÙ   rM   rK   rN   c                 s   s   | ]}|d  V  qdS r  r   r  r   r   r   rß   $  r,   zsummary_body.<locals>.<genexpr>rƒ   rÂ   c                 s   s"   | ]}|d  s|d sdV  qdS )r¹   rz   r   Nr   r$  r   r   r   rß   &  r,   zU<html><body style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#111">zB<h2 style="margin:0 0 3px;font-size:17px">Soccer xlow tracker</h2>r\  r  z' &middot; full dashboard attached</div>r/   Nrž  z7<table style="border-collapse:collapse;font-size:14px">c                    s   ˆ   d| › d|› d�¡ d S )Nz2<tr><td style="padding:3px 12px 3px 0;color:#555">z"</td><td style="padding:3px 0"><b>r)  rï   )r!  rP  ©rI  r   r   rH   /  s    ÿzsummary_body.<locals>.rowzKalshi totalzPoly-US totalzAt riskz4 &nbsp;<span style="color:#777;font-weight:normal">(z open)</span>zSignals (today+tmrw)z6 &nbsp;<span style="color:#c1272d;font-weight:normal">z not placed</span>rb   zForward testzn=r   r]  r  z
 &nbsp; z=r  r  zWR vs impliedr  ru  r  r  r^  r  rd  z®<div style="color:#888;font-size:12px;margin-top:14px">Open the attached <b>dashboard.html</b> for signals, positions, per-league forward test and league wiring health.</div>r;  r<  )rk   r  r–   r„   r   rü   rý   r   rd   r*   rC   r   rþ   r>  r[  rA   rZ  r@  )r  r•   rC  rD  rF  rG  rH  rJ  Zuntradedrh  ri  rH   r   rw  r   Úsummary_body  sT    ÿþ$
ÿ
$&


ÿ
ÿþÿÿ



rx  Ú__main__z--emailr   r   rƒ   rÂ   z
DASHBOARD r4  z	 | fwd n=r   z ROI r  r  r6  zdashboard.htmlzemailed (attachment): z--email-inline)Ú
send_emailzemailed (inline fallback)zwrote z  (r  z bytes))N)r
   )r   )N)r  )NN)r   )r  )OÚ__doc__rr   r   ÚsysÚosr   r   r   r   Úpathlibr   Úcollectionsr   ru   Úinsertro   rÕ  ZOUTÚargvr   ZBAND_LOZBAND_HIrò  r?  r@  r   r   rJ   rk   ry   r„   r–   r‹   r€   r±   r   r  rH  r[  r…  rŒ  rŠ  rp  r“  r­  r²  r¹  rÀ  rÊ  rÙ  r=  rA  r�  rç  rè  rì  rð  r  rT  rg  ra  rf  rZ  r[  rj  rv  rx  rg  ZfullZ
write_textr*   rJ  rþ   r>  Zsubjr“   rz  rq  rA   r   r   r   r   Ú<module>   s¸   
	:e
E&/   I/!Z 
>C = 



q         `

 /
 
