a
    ØN~j]T  ã                	   @   s  d Z ddlZddlZddlZddlmZ ddlmZ ej	 
dd¡ ddlZedƒZedƒZg d¢Zejd	d
ddddejjd�Zejd	d
ddddejjd�Zd)dd„Zdd„ Zdd„ Zd*dd„Zdd„ Zdd„ Zd d!„ Zd"d#„ Zd$d%„ Zd+d&d'„Zed(k�r eƒ  dS ),aÀ  
Settle the PAPER market-maker quotes logged by mm_quoter.py, and email results.
Places nothing, cancels nothing, touches no money.

HOW A FILL IS DECIDED (fill_from_quotes): a MANAGED resting order that cancels
and reposts every quoting cycle. Walks a leg's full quote history and, for the
ask we were showing during each interval, checks whether any REAL trade printed
at or above it -- strictly BEFORE kickoff, since we cancel at kickoff (user
decision, 2026-07-26) rather than leave orders resting through the match.

THIS REPLACES A BROKEN EARLIER VERSION, kept here as a warning: the original
test used only the FIRST logged ask with no time bound, comparing timestamp
STRINGS (which mixed +00:00/Z/-04:00 suffixes and silently matched almost
nothing correctly). Combined, those two bugs first UNDER-counted fills to
near-zero, then, once one was fixed without the other, OVER-counted by
including in-play trades -- a stale ask getting lifted after a goal, by someone
watching the match. Reported fill rate swung 13% -> 83% across those bugs
before landing at a real ~19% (fixed-margin ask) to ~61% (ask capped at the
market's own ask) once both were corrected. Verify against
research/mm_decile_by_volume.py's sanity checks before trusting any number this
script prints.

STILL DELIBERATELY OPTIMISTIC, and the number to keep distrusting: this ignores
queue position. In reality several makers may sit at the same price and the
buyer takes whoever is first, so the realised fill rate would be lower and
plausibly skewed toward the fills nobody else wanted. Capture is the ONE
question history cannot answer (see [[project_market_making_early_edge]]) --
this measures a ceiling, and the gap between it and a live test is the thing
worth knowing.

P&L per contract sold = our_ask - settlement (1 if that outcome happened, 0
otherwise). Makers pay no Kalshi fee -- verified on a real maker fill (7.8
contracts @ 0.36, fee_cost 0.0000).

Usage:  python main\mm_settle.py [--email]
é    N)Údefaultdict)ÚPathzE:\soccer\mainz&E:\soccer\research\mm_paper_quotes.csvz'E:\soccer\research\mm_paper_results.csv)ÚtsÚdivÚmatchÚ
leg_tickerÚsideÚour_askÚcompetitiveÚfilledÚfill_pxÚsettledÚ	pnl_centsÚsizeÚregimeÚvolumeiê  é   é   é   é9   é2   )Ztzinfoé   é   é   c                 C   s8   |r|S t | ƒ}|du rdS |tkr(dS |tkr4dS dS )zCRegime for a row. Prefers the `regime` column written at post time.NÚv1Úv3Úv2)Ú_ptÚREGIME_V3_STARTÚREGIME_V2_START)Zts_strZtaggedÚt© r!   úE:\soccer\main\mm_settle.pyÚ	regime_ofY   s    r#   c                 C   s4   zt j t| ƒ dd¡¡W S  ty.   Y dS 0 dS )a  Parse any of our timestamp flavours to an aware datetime.

    Quote times are UTC with +00:00, kickoffs are ET with -04:00, Kalshi trades
    are UTC with Z. Comparing these as STRINGS is meaningless and silently
    filtered out every trade at one point -- always parse.
    ÚZz+00:00N)ÚdtÚdatetimeZfromisoformatÚstrÚreplaceÚ	Exception)Úsr!   r!   r"   r   e   s    r   c              
   C   sü   t dd„ | D ƒdd„ d�}t|ƒD ]Ô\}}t|d ƒ}|du sF||krJ qø|d t|ƒk rnt||d  d ƒn|}|du s‚||kr†|}zt|d	 ƒ}W n ttfy°   Y q"Y n0 |D ]>\}	}
||	  krÒ|k r¶n q¶|
|d
 kr¶d||	f    S q¶q"dS )aw  Simulate a MANAGED resting order: cancel-and-repost every cycle.

    Walks the leg's quote history in order. For each interval between
    consecutive quotes (bounded above by kickoff), asks whether any trade
    printed at or above the ask WE WERE SHOWING during that interval. First hit
    is the fill.

    WHY THIS AND NOT "did anything ever trade above our first ask":
      - the old test used only the FIRST ask and no upper time bound, so it
        modelled an order posted once and abandoned, and counted IN-PLAY trades.
        That reported an 83% fill rate; the true pre-kickoff figure was 13%.
      - a real maker re-quotes as fair value drifts. If fair moves 27c -> 25c and
        we repost, an order that was hopeless at 29c becomes live. The static
        model never sees that and under-counts genuine fills.
      - we CANCEL AT KICKOFF (user decision, 2026-07-26). Leaving orders resting
        through a match is a different strategy and was measured at -$23.60:
        prices jump on goals and stale asks get lifted by people watching.

    Returns (filled: bool, fill_px: float|None, fill_ts: datetime|None).
    c                 S   s   g | ]}|  d ¡r|‘qS )r	   ©Úget)Ú.0Úrr!   r!   r"   Ú
<listcomp>‡   ó    z$fill_from_quotes.<locals>.<listcomp>c                 S   s   t | d ƒS ©Nr   ©r'   )r.   r!   r!   r"   Ú<lambda>ˆ   r0   z"fill_from_quotes.<locals>.<lambda>©Úkeyr   Né   r	   g•Ö&è.>T)FNN)ÚsortedÚ	enumerater   ÚlenÚfloatÚ	TypeErrorÚ
ValueError)Ú
quote_rowsÚtradesÚkickoffÚrowsÚir.   ÚstartÚendZaskr    Úpxr!   r!   r"   Úfill_from_quotesr   s$    ÿ(
$rE   é@œ  c              
   C   s  g d }}t |ƒ|k r¦| ddœ}|r,||d< zt d|¡}W n tyR   Y q¦Y n0 | dg ¡}||7 }| d¡}|r¦|s|q¦tdd„ |D ƒdd	�}|r
|r
||kr
q¦q
g }	|D ]`}
t|
 d
¡ƒ}|r®|r®||kr®z |	 |t|
 d¡pædƒf¡ W q® t	t
f�y   Y q®Y q®0 q®|	S )a  Every trade on this leg after `since_dt`, as (datetime, price) tuples.

    No upper time bound is applied here -- fill_from_quotes()'s own interval
    logic (each interval capped at kickoff) already excludes in-play trades,
    so fetching everything after the first quote and letting that function
    filter is simpler and cannot reintroduce the in-play-counted-as-a-fill bug.

    PAGINATE UNTIL WE REACH since_dt, don't stop at a fixed count (fixed
    2026-07-27). The API returns trades NEWEST FIRST, and soccer legs take
    thousands of in-play trades in the minutes around a goal -- Pachuca had
    17,999. The old cap=2000 therefore filled entirely with post-kickoff
    activity and never reached back to the pre-kickoff window, so every
    pre-KO fill on a busy leg was invisible. That made the settler report
    10 of 21 legs filled (and a -$9.77 day built from phantom naked legs)
    when the true count was far higher. Stop when the oldest trade fetched
    predates since_dt; `cap` is now only a runaway guard.
    NéÈ   )ÚtickerÚlimitÚcursorz/markets/tradesr>   c                 s   s*   | ]"}t | d ¡ƒrt | d ¡ƒV  qdS )Úcreated_timeN)r   r,   ©r-   Úxr!   r!   r"   Ú	<genexpr>»   s   ÿz trades_tuples.<locals>.<genexpr>)ÚdefaultrK   Zyes_price_dollarsr   )r9   ÚktZ_getr)   r,   Úminr   Úappendr:   r;   r<   )rH   Zsince_dtZcapÚoutrJ   ÚpÚdZtrZoldestZkeeprM   r    r!   r!   r"   Útrades_tuplesš   s8    



ÿ rV   c                 C   sT   z<t  | ¡ di ¡}| d¡}|dkr,W dS |dkr:W dS W n tyN   Y n0 d S )NZmarketÚresultZyesr6   Znor   )rP   Z
get_marketr,   r)   )rH   Úmr.   r!   r!   r"   Ú
settlementÊ   s    

rY   c                  C   s¼  t  ¡ stdƒ d S tt tt ddd�¡ƒ} ttƒi i   }}}| D ]b}|d }| ||¡ t	| 
dd¡ƒ d¡r˜||vsŽ|d || d krD|||< qD||  |¡ qDt|ƒt|ƒB }tt| ƒ› d	t|ƒ› d
t|ƒ› d�ƒ g }|D �]è}t|ƒ}	|	d u rþqæ| 
|¡}
|
�rªt|
d ƒ}d| d|  d }| t|
d |
d |
d › d|
d › �||
d |dd||	t||	 d | dƒ|
 
dd¡|
 
dd¡t|
d |
 
d¡ƒd�¡ qæ| 
|g ¡}|�s¾qæ|| }t| 
d¡ƒ}tt|dd„ d�d ƒ}|d u sæ|d u �r qæt||ƒ}t|||ƒ\}}}|�r.||	 d nd }| t|d |d |d › d|d › �||d |�rj|nt|d d ƒ|d  
d!d¡t|ƒ|�r–|nd|	t|dƒ| 
dd¡| 
dd¡t|d | 
d¡ƒd�¡ qættd"ddd��<}tj|td#�}| ¡  |D ]}| |¡ �qþW d   ƒ n1 �s&0    Y  t|| ƒ}t||ƒ}t|ƒ d$t j!v �r¸z*dd%l"m#} |d&d'|› d(�ƒ td)ƒ W n4 t$�y¶ } ztd*|› �ƒ W Y d }~n
d }~0 0 d S )+Nzno paper quotes logged yetÚ zutf-8)ÚnewlineÚencodingr   ÚreasonZCOMPLETE_AT_BIDr   z quote rows -> z distinct legs (z completed at bid)r	   çìQ¸…ë±?r6   éd   r   Úhomez vs Zawayr   Úhedgeé   r   r   r   r   )r   r   r   r   r   r	   r
   r   r   r   r   r   r   r   r?   c                 S   s   | d S r1   r!   )rM   r!   r!   r"   r3   
  r0   zmain.<locals>.<lambda>r4   ç        r
   Úw)Z
fieldnamesz--email)Ú
send_emailzPaper market-maker resultsz<pre>z</pre>Zemailedzemail failed: )%ÚQUOTESÚexistsÚprintÚlistÚcsvZ
DictReaderÚopenr   Ú
setdefaultr'   r,   Ú
startswithrR   Úsetr9   rY   r:   ÚdictÚroundr#   r   rQ   rV   rE   ÚintÚRESULTSZ
DictWriterÚRFIELDSZwriteheaderZwriterowÚbackfill_completionÚ	summariseÚsysÚargvZdaily_signalsre   r)   )r@   Úby_legZ
completionÚmetar.   ÚkÚlegsrS   Zlegr*   Zcomp_rowrD   Úfee_cZqrowsZr0r?   Zfirst_tsr>   r   Z_ftsÚpnlZfhrd   ÚoÚhedgedÚbodyre   Úer!   r!   r"   Úmain×   s�    	ÿ





ù

ø

.

r‚   c                 C   s¶  t tƒ}|D ]}||d   |¡ qt tƒ}| D ]}||d |d f  |¡ q0g }| ¡ D �]R\\}}}	t|	ƒdkr€||	7 }q\dd„ |	D ƒ}
t|
ƒdks¬tdd	„ |	D ƒƒr¶||	7 }q\|
d
 }t| |d g ¡dd„ d�}|sè||	7 }q\zt|d d ƒ}W n$ t	t
f�y    ||	7 }Y q\Y n0 d
|  k �r:dk �sFn ||	7 }q\d| d|  d }|	D ]P}||u �r¢t||d|dt||d  d | dƒd�}| |¡ n
| |¡ �q^q\|S )a«  What the CURRENT completion rule would have produced on legacy books.

    Books settled before mm_quoter learned to complete are left unhedged in the
    recorded results. This applies the rule retroactively so the rule can be
    judged -- and returns a SEPARATE list rather than editing `out`, because
    overwriting recorded P&L with a later rule is how track records become
    unfalsifiable.

    LEGITIMATE because the rule is outcome-independent: "if exactly one of three
    legs is unfilled, sell it into the bid" never looks at who won. It is not
    dropping losing bets; it is closing an implementation gap.

    THE ONE REAL CAVEAT: the bid used is the LAST one we logged before kickoff.
    Hedging live would happen at whatever bid existed at the decision moment,
    plausibly a cent or two worse. So this is optimistic, and the gap between it
    and future live-rule results is the honest measure of that optimism.
    r   r   r   é   c                 S   s   g | ]}|d  s|‘qS ©r   r!   rL   r!   r!   r"   r/   J  r0   z'backfill_completion.<locals>.<listcomp>r6   c                 s   s   | ]}|d  dv V  qdS )r
   )ra   úhedge(backfill)Nr!   rL   r!   r!   r"   rN   K  s   ÿz&backfill_completion.<locals>.<genexpr>r   c                 S   s   | d S r1   r!   )Úyr!   r!   r"   r3   P  r0   z%backfill_completion.<locals>.<lambda>r4   éÿÿÿÿZk_bidr^   r_   r…   r   rb   )r	   r   r   r
   r   )r   ri   rR   Úitemsr9   Úanyr7   r,   r:   r;   r<   ro   rp   )rS   r=   rx   r.   Zby_matchr~   r   r   r   r{   ZunfZtgtr@   Zbidr|   rM   r†   r!   r!   r"   rt   ,  sP    ÿ


þrt   c                 C   s@   dd„ | D ƒ}t dd„ |D ƒƒ}t dd„ |D ƒƒ}t|ƒ||fS )Nc                 S   s   g | ]}|d  r|‘qS r„   r!   ©r-   r~   r!   r!   r"   r/   i  r0   z_totals.<locals>.<listcomp>c                 s   s   | ]}t |d  pdƒV  qdS ©r   r   N©r:   rŠ   r!   r!   r"   rN   j  r0   z_totals.<locals>.<genexpr>c                 s   s&   | ]}|d  t |d pdƒ V  qdS ©r   r   r   NrŒ   rŠ   r!   r!   r"   rN   k  r0   )Úsumr9   )r@   ÚfÚctsr}   r!   r!   r"   Ú_totalsh  s    r‘   c              
   C   sÊ  t | ƒ}|s||ddg7 }dS dd„ | D ƒ}tdd„ | D ƒƒ}||d|› �d	|› d
|| d d›d�dt |ƒ› d
t |ƒ| d d›d�g7 }|�r¼tdd„ |D ƒƒ}tdd„ |D ƒƒ}|d|d›�d|d d›d�g7 }|rô| d|| d›d�¡ tdd„ ƒ}|D ]Z}	||	d  d  |	d t|	d �p,dƒ 7  < ||	d  d  t|	d �pTdƒ7  < �q| d ¡ t| ¡ d!d„ d"�D ]<\}
\}}|�r~| d#|
d$›d%|d&›d'|| d(›d)�¡ �q~| d¡ dS )*z'Render one regime's stats into `lines`.z  (no settled legs yet)rZ   Nc                 S   s   g | ]}|d  r|‘qS r„   r!   rŠ   r!   r!   r"   r/   u  r0   z_block.<locals>.<listcomp>c                 s   s"   | ]}t |d  ƒdkrdV  qdS )r
   Ú1r6   Nr2   rŠ   r!   r!   r"   rN   v  r0   z_block.<locals>.<genexpr>z  legs quoted         : z  competitive         : z (r_   z.0fz%)z  would have filled   : c                 s   s&   | ]}|d  t |d pdƒ V  qdS r�   rŒ   rŠ   r!   r!   r"   rN   |  r0   c                 s   s   | ]}t |d  pdƒV  qdS r‹   rŒ   rŠ   r!   r!   r"   rN   }  r0   z  contracts sold      : z,.0fz  P&L                 : z+,.2fú dollarsz  per contract        : z+.2fz centsc                   S   s   ddgS )Nrc   r!   r!   r!   r!   r"   r3   ‚  r0   z_block.<locals>.<lambda>r   r   r   r   r6   z  by league:c                 S   s   | d d  S )Nr6   r   r!   )Zkvr!   r!   r"   r3   ‡  r0   r4   z    Z6sú z8,.0fz contracts  z+7.2fú c/contract)r9   rŽ   rR   r   r:   r7   rˆ   )r@   ÚtitleÚlinesÚnÚfillsZcompZtotr�   Zbyr~   rU   rT   Úcr!   r!   r"   Ú_blocko  s8    "ýÿ.*
 ,r›   c                    sì  ˆsdS d}‡fdd„|D ƒ‰ ‡ fdd„|D ƒ\}}}}t dd„ ˆD ƒt|ƒ ƒ}d	d
g}t|d|ƒ | d¡ | d¡ t|d
|ƒ |r¨| d
¡ | d¡ | d¡ t|d
|ƒ |rÐ|g d¢7 }t|d
|ƒ |D ]2‰|dˆ› d�dg7 }t‡fdd„ˆD ƒd
|ƒ qÔtˆƒ}	dd„ ˆD ƒ}
|�rÖtˆƒ\}}}t|ƒ\}}}t|| ƒdk�rÖ|d
ddddd|�rŒd|d›d|d d›d || d!›d"�nd
|�rºd#|d›d|d d›d || d!›d"�nd
d$|| d d›d%�g	7 }|g d&¢7 }d' |¡S )(NziPAPER MARKET MAKER

No settled paper quotes yet. Results appear after the next batch of matches finishes.)Úv4r   r   r   c                    s    i | ]‰ ˆ ‡ fd d„ˆD ƒ“qS )c                    s   g | ]}|  d ¡ˆ kr|‘qS ©r   r+   rŠ   ©Úgr!   r"   r/   �  r0   z(summarise.<locals>.<dictcomp>.<listcomp>r!   )r-   )rS   rž   r"   Ú
<dictcomp>�  r0   zsummarise.<locals>.<dictcomp>c                 3   s   | ]}ˆ | V  qd S )Nr!   )r-   rŸ   )Úby_regr!   r"   rN   ž  r0   zsummarise.<locals>.<genexpr>c                 S   s   h | ]}|  d ¡pd’qS )r   ú?r+   rŠ   r!   r!   r"   Ú	<setcomp>Ÿ  r0   zsummarise.<locals>.<setcomp>u$   PAPER MARKET MAKER â€” Kalshi soccerrZ   zzv4 CURRENT RULE (v3 plus the MIN_LEG_VOLUME liquidity gate: quote only books whose thinnest leg has traded volume >= gate)ú>--------------------------------------------------------------uJ   v3 (delayed completion, NO liquidity gate) â€” DIFFERENT RULE, DO NOT POOLuY   v2 (crossed to complete IMMEDIATELY at 2-of-3, ~9h early) â€” DIFFERENT RULE, DO NOT POOL)r¤   u<   v1 LEGACY â€” DIFFERENT STRATEGY, DO NOT POOL WITH THE ABOVEzC(flat fair+2c, uncapped so it sat above the market on ~86% of legs,z1 3h window, 5-min cron. Kept only for reference.)u8    â€” UNRECOGNISED REGIME, not described in mm_settle.py.z@Add a labelled block for it; do not pool it with anything above.c                    s   g | ]}|  d ¡ˆ kr|‘qS r�   r+   rŠ   rž   r!   r"   r/   º  r0   zsummarise.<locals>.<listcomp>c                 S   s   g | ]}|d  r|‘qS r„   r!   rŠ   r!   r!   r"   r/   ½  r0   g      à?z:----------------------------------------------------------z0WITH THE COMPLETION RULE APPLIED TO LEGACY BOOKSz=(books that settled before mm_quoter could hedge; the rule isz> outcome-independent, so this is a fair retrospective, but thez6 bid used is the LAST one logged, so it is OPTIMISTIC)z  as recorded      : Z3dz fills  r_   z+8.2fz
 dollars  z+6.2fr•   z  rule applied     : z  difference       : r“   )rZ   z@CAVEAT: fills assume we win every trade that printed at or abovezAour ask. Real queue position would give fewer and worse fills, soz+treat this as the CEILING, not an estimate.Ú
)r7   rn   r›   rR   r9   r‘   ÚabsÚjoin)rS   r   Zknownrœ   r   r   r   ZstrayÚLr˜   r™   ZnaZcaZpaZnhZchZphr!   )r¡   rŸ   rS   r"   ru   �  sh    ÿ




þÿÿÿÿ÷
ru   Ú__main__)N)rF   )N)Ú__doc__rv   rj   r&   r%   Úcollectionsr   Úpathlibr   ÚpathÚinsertZkalshi_traderrP   rf   rr   rs   ÚtimezoneZutcr   r   r#   r   rE   rV   rY   r‚   rt   r‘   r›   ru   Ú__name__r!   r!   r!   r"   Ú<module>   s0   $	
(
0U<
E
