a
    $œ�jàC  ã                   @   s°   d Z ddlZddlZddlZddlmZ dZdZdZ	dZ
g d¢Zd	d
dddddddddddœZdd„ Zdd„ Zdd„ Zg d¢Zdd„ Zdd „ Zd!d"„ Zd#d$„ Zed%kr¬eƒ  dS )&aN  Build a SMALL, downloadable fill-analysis dataset from mm_live_legs.csv.

WHY THIS EXISTS. mm_live_legs.csv is ~31MB and growing ~10MB/day, so the
dashboard has to gzip it to embed it, and a .gz is exactly what an outside
analysis environment tends to refuse. This writes a plain CSV small enough to
embed and hand to anything.

TWO THINGS IT FIXES BESIDES SIZE:

1. ONE ROW PER RESTING EPISODE, NOT PER RE-POST. The legs log writes a row every
   time a leg is re-posted, so a leg that rested for an hour contributes ~50 rows
   and one that filled in 90s contributes one. Treating those as independent
   observations is a trap: it weights the sample toward legs that DID NOT fill,
   which is the outcome being predicted. Collapsing to (ticker, price) episodes
   makes each resting order one observation and moves exposure into its own
   column, where it belongs as a feature.

2. FEATURES ONLY EXIST FROM 2026-08-18. ask/bid/mid/pin_fair/league logging was
   added mid-stream: 0% of 08-16 rows have them, 21.7% of 08-17, 100% from 08-18.
   Rows without them cannot support the analysis and are dropped rather than
   silently becoming empty cells that a model reads as zeros.

The outcome column is `filled`, joined from mm_live_realfills.csv on
(ticker, price_cents).

Usage:  python research\mm_fill_dataset.py
Writes: research\mm_fill_dataset.csv
é    N)Údefaultdictz#E:\soccer\research\mm_live_legs.csvz(E:\soccer\research\mm_live_realfills.csvz&E:\soccer\research\mm_fill_dataset.csvz
2026-08-18))Úts_firstz<When this resting order first went on the book (local time).)Úminutes_exposedzvHow long it stayed on the book across all re-posts. A feature, not an outcome: longer exposure is more chance to fill.)Ún_postsz�How many re-posts were collapsed into this one row. Orders carry a 90s TTL and are refreshed at 45s, so this is roughly minutes_exposed/0.75.)Úleaguez"Division code (E0, MLS, AR1, ...).)Úeventz!Kalshi event ticker -- the match.)Útickerz6Kalshi market ticker -- the individual leg being sold.)Úsidez3Leg abbreviation: a team code, or TIE for the draw.)Úprice_centsz9Our ask, in cents. This is the price the order rested at.)Úask_at_postz&Best ask on the market when we posted.)Úbid_at_postzBest bid when we posted.)Úmid_at_postzMid when we posted.)Úspread_czxask_at_post - bid_at_post, in cents. Mostly 1c: the venue prices on a 1-cent grid, so this is usually at the tick floor.)Úpin_fairzrPinnacle fair probability for this outcome (overround removed). The independent estimate of what the leg is worth.)Úask_vs_pin_cz‡Our ask minus pin_fair, in cents. Positive = we are asking above what Pinnacle thinks it is worth, which is the maker edge on that leg.)Úabove_touch_czÛCents ABOVE the best ask we quoted. 0 = at the touch. Non-zero comes from the budget rule: once part of a book has filled, the rest may only be sold at prices that still clear the payout, so a stranded book quotes high.)Úqueue_aheadzÒContracts already resting at our price when we posted. The number the whole experiment exists to calibrate -- a fill behind a large queue means the queue cleared, not that we were competitive. Median is ~4,800.)Úbid_sizezDepth on the bid at post.)ÚvolumezContracts traded on this leg.)Ú
volume_24hz-Contracts traded on this leg in the last 24h.)Úbook_volumezšContracts traded across ALL THREE legs of the match. The strongest single predictor of book completion found so far (+0.091 AUC); min-leg volume was null.)Úopen_interestzOpen interest on this leg.)Ú	hrs_to_koz_Hours until kickoff at post. Negative would be in play, but new books are never opened in play.)Ú	collectedzuWhat this BOOK had already banked from earlier fills, per contract, when this leg was posted. 0 = nothing filled yet.)ÚtargetzcWhat the book must collect in total to clear the $1.00 payout plus fees plus margin. Roughly 1.017.)ÚscalezÍtarget-minus-collected divided by the remaining legs at the touch. >1 means the touches do NOT cover what the book still needs -- which gates a new book and forces a committed one to quote above the touch.)Úfreshz|1 if this was the first time this leg had ever been offered, 0 if it was already in the offered set from an earlier session.)Úfilledz�OUTCOME. 1 if the order was filled, 0 if it expired or was cancelled. Joined from mm_live_realfills.csv on (ticker, price_cents).)Úfill_qtyzFContracts filled, blank if never filled. Kalshi can fill fractionally.)Úseconds_to_fillz1Seconds from post to fill, blank if never filled.)Úis_xlowzð1 if the frozen xlow rule ALSO fired on this exact fixture and side, 0 if not. Both signal rules are home/away only, so a draw leg is always 0. BLANK means the fixture could not be resolved (unmapped team abbreviation) -- unknown, not "no".)Úis_xhighzæ1 if the frozen xhigh rule ALSO fired on this exact fixture and side, 0 if not. Same blank/0 convention as is_xlow. xhigh overlaps xlow heavily -- both fire on Kalshi CLV in the same band -- so the two columns are not independent.é   é   é   é   é   é   é   é   é	   é
   é   é   )ZJANZFEBZMARZAPRZMAYZJUNZJULZAUGZSEPZOCTZNOVZDECc                 C   sŠ   ddl }| pd dd¡d }| d|¡}|r:| d¡tvr>d	S | d¡| d¡| d
¡| d¡f\}}}}d|› dt| d›d|› �|fS )a~  'KXEPLGAME-26AUG23MCIBOU' -> ('2026-08-23', 'MCIBOU'), or (None, None).

    The Kalshi event code packs the date and BOTH team abbreviations with no
    separator, and the abbreviations are 2-4 characters, so the pair cannot be
    split on width. It does not need to be: the HOME abbr is a prefix of the
    pair and the AWAY abbr is a suffix, which is enough to place a leg.
    r   NÚ ú-r"   éÿÿÿÿz%^(\d{2})([A-Z]{3})(\d{2})([A-Z0-9]+)$r#   ©NNr$   r%   Z20Z02d)ÚreÚrsplitÚmatchÚgroupÚ_MONTHS)r   r2   ÚtailÚmZyyZmonZddÚteams© r:   úresearch/mm_fill_dataset.pyÚ_parse_eventw   s    ,r<   c                 C   sJ   | r|sdS |dkrdS |   |¡|  |¡ }}|r:|s:dS |rF|sFdS dS )zF'home' | 'draw' | 'away' for a leg abbreviation, or None if ambiguous.NZTIEÚdrawÚhomeÚaway)Ú
startswithÚendswith)r9   ÚlegZstartsZendsr:   r:   r;   Ú	_leg_sideˆ   s    rC   c               
   C   sœ  zddl } ddl}W nB tyV } z*tdt|ƒj› d|› d�ƒ W Y d}~dS d}~0 0 tƒ tƒ  }}zè| jdd�D ]Ö}|d	 rŒ|d	 d ni }| d
¡| d¡ }}|rt|s°qt| 	|¡| 	|¡ }	}
|	rt|
sÐqt|d |d |	› |
› � 
¡ f}|  |¡}|�r| d¡�r| ||d f ¡ |  |¡}|rt| d¡rt| ||d f ¡ qtW nD t�y’ } z*tdt|ƒj› d|› d�ƒ W Y d}~dS d}~0 0 ||fS )a¯  {(game_date, league, teams_code, side)} for xlow and for xhigh.

    WHY THIS JOIN EXISTS. The maker quotes all three legs of a book blind to
    what the signal rules think. Tagging each leg with whether xlow or xhigh
    also fired on that exact fixture AND side makes it possible to ask whether
    a leg the signal rules call mispriced behaves differently -- fills faster,
    completes more often -- which is a question neither dataset can answer
    alone.

    Keyed on the FIXTURE, not on (date, league): several matches in one league
    share a date, and a coarser key would tag the wrong game. The Kalshi
    abbreviations come from team_map via maps.kalshi_abr, so a team missing
    there simply produces no key and the row is left blank rather than a
    confident 0.

    Returns ({}, {}) rather than raising if anything is unavailable -- this is
    an annotation on a dataset, and it must never stop the dataset building.
    r   Nz"  [warn] signal join unavailable (z: z!); is_xlow/is_xhigh will be blankr1   F)Zrequire_resultZprer>   r?   Úgdr   r	   z  [warn] signal join failed ()Zforward_test_unique_bookÚmapsÚ	ExceptionÚprintÚtypeÚ__name__ÚsetZload_fixturesÚgetZ
kalshi_abrÚupperZ	fire_xlowÚaddZfire_xhigh_detail)ÚFrE   ÚeZxlowZxhighZfxZr0ÚhÚaZhaZaaÚkeyÚbÚdr:   r:   r;   Úsignal_hits–   s6    

rU   ©r   r   r   r   r   r   r	   r
   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r    r!   c              	   C   s@   t | dƒ ¡ }d|v r"| dd¡}tt t |jddd�¡¡ƒS )z@Read a CSV that may contain NUL bytes from an interrupted write.Úrbó    ó    úutf-8Úreplace)Úerrors)	ÚopenÚreadr[   ÚlistÚcsvZ
DictReaderÚioÚStringIOÚdecode)ÚpathÚrawr:   r:   r;   Ú_readÕ   s    rf   c              	   C   s(   z
t | ƒW S  ttfy"   Y d S 0 d S )N)ÚfloatÚ	TypeErrorÚ
ValueError)Úvr:   r:   r;   Ú_fÝ   s    
rk   c                 C   s~   | du rdS t | dd¡ƒ\}}|p&d dd¡d }t||ƒ}|rN|rN|du rRdS |dkr^dS || d	d¡||f| v rzdS dS )
ao  1 / 0 / '' for whether a signal rule fired on this exact leg.

    BLANK, NOT ZERO, when the fixture cannot be resolved -- an unmatched team
    abbreviation or an unparseable event code is 'unknown', and writing 0 there
    would tell a model the rule declined this leg when in fact nobody asked.
    Both rules are home/away only, so a draw leg is a genuine 0.
    Nr.   r   r/   r"   r0   r=   r   r   )r<   rK   r3   rC   )ÚhitsrQ   r   rD   r9   rB   r	   r:   r:   r;   Ú_hitä   s    
rm   c            #   %   C   sà  t ƒ \} }ttƒ}ttƒD ]L}z|d tt|d ƒƒf}W n ttt	fyV   Y qY n0 ||  
|¡ qttƒ}tdt|ƒd›�ƒ dd„ |D ƒ}tdt› dt|ƒd›�ƒ i }|D ]°}ztt|d ƒƒ}W n tt	fyæ   Y q´Y n0 |d |f}| |¡}	|	d u �r||d	d
œ||< q´|	d  d	7  < |d |	d d k�rH||	d< |d |	d d k r´||	d< q´tdt|ƒd›�ƒ g }
d}| ¡ D �]T\\}}}	|	d |	d  }}d  }}d }z4tj |d ¡}tj |d ¡}||  ¡ d }W n tt	f�y   Y n0 t| d¡ƒt| d¡ƒ }}t| d¡ƒ}|d u�rP|d u�rP|| d nd }|d u�rn|d | d nd }| ||f¡�p„g }g }|�rš|�rš|ng D ]¨}| d¡�p°d}tjdd�}|�sà| d¡�pÒd}tjdd�}|�sê�qžztj |¡}W n t	�y   Y �qžY n0 || |  k�r6|| k�ržn n
| 
|¡ �qž|�rV|d	7 }tdd„ |D ƒƒ}|�r€t|d  d¡ƒnd }|
 
|d |d u�r |d›nd|	d | d d¡| d!d¡|| d"d¡|| dd¡| dd¡| d#d¡|d u�rú|d›nd| dd¡|d u�r|d›nd| d$d¡| d%d¡| d&d¡| d'd¡| d(d¡| d)d¡| d*d¡| d+d¡| d,d¡| d-d¡| d.d¡| d/d¡|�rœd	nd|�r¬|d›nd|d u�rÀ|d0›ndt| ||ƒt|||ƒd1œ¡ �qŠ|
jd2d3„ d4� ttd5dd6d7��0}tj|td8�} |  ¡  |   |
¡ W d   ƒ n1 �s:0    Y  dd l!}!|!j" #t¡}"tƒ  td9t› �ƒ td:t|
ƒd›�ƒ td;|d›d<d| t$t|
ƒd	ƒ d0›d=�ƒ td>|"d? d›d@�ƒ tdA|"dB d? d›d@�ƒ d S )CNr   r
   zlegs rows read      : ú,c                 S   s*   g | ]"}|  d ¡pddd… tkr|‘qS )Útsr.   Nr+   )rK   ÚFEATURES_FROM)Ú.0Úrr:   r:   r;   Ú
<listcomp>  rY   zmain.<locals>.<listcomp>zrows >= z  : r"   )ÚfirstÚlastÚnrv   ro   ru   rt   zresting episodes    : r   g      N@r   r   r   éd   g      Y@Z	posted_atr.   é<   )Zsecondsi¤  c                 s   s    | ]}t | d ¡ƒpdV  qdS )Úcountr   N)rk   rK   )rq   Úxr:   r:   r;   Ú	<genexpr>O  rY   zmain.<locals>.<genexpr>r   z.2fr   r   r	   r   r   r   r   r   r   r   r   r   r   r   r   r   z.1frV   c                 S   s   | d S )Nr   r:   )rr   r:   r:   r;   Ú<lambda>t  rY   zmain.<locals>.<lambda>)rR   ÚwrZ   )ÚnewlineÚencoding)Z
fieldnameszwrote z  rows          : z  filled        : z  (z%)z  size          : g    €„.Az MBz  base64 embed  : gq=
×£põ?)%rU   r   r_   rf   ÚFILLSÚintrg   ÚKeyErrorrh   ri   ÚappendÚLEGSrG   Úlenrp   rK   ÚitemsÚdtÚdatetimeZfromisoformatZtotal_secondsrk   Z	timedeltaÚsumrm   Úsortr]   ÚOUTr`   Z
DictWriterÚOUT_COLSZwriteheaderZ	writerowsÚosrd   ÚgetsizeÚmax)#Z	xlow_hitsZ
xhigh_hitsZfillsrr   rR   ZlegsZkeptZepZpcrO   ZrowsZn_filledZtkrrQ   rS   Zt0Út1ZminsZaskZbidZpinZspreadZ
ask_vs_pinZcandZflrz   ZstampZslackZxdZqtyZstfZfhr}   r�   Úsizer:   r:   r;   Úmainø   sÜ    





$$




















á"*,r’   Ú__main__)Ú__doc__r`   ra   rˆ   r‡   Úcollectionsr   r„   r€   r‹   rp   Z
FIELD_DOCSr6   r<   rC   rU   rŒ   rf   rk   rm   r’   rI   r:   r:   r:   r;   Ú<module>   s.   
Bÿ3 