a
    WP~jo ã                -   @   sh  d Z ddlZddlZddlZddlZddlZddlZddlZddl	m
Z
 ej dd¡ ddlZddlZddlmZmZmZ ddlZe
dƒZe
dƒZdZd	Zd
ZdZdZdxdd„ZdZdZdZ dZ!dZ"da#da$dd„ Z%dZ&dZ'dd„ Z(ddddd d!d"d#d$d$d$d%d&d'd(d)d*d+d,d-d.d/d0d1d2d2d2d2d2d2d2d2d2d3d3d4d4d5d5d6d6d7d8d9d:œ,Z)d;d<„ Z*d=d>„ Z+i Z,d?Z-d@dA„ Z.dBdC„ Z/dDZ0g dE¢Z1dFdG„ Z2dHdI„ Z3e
dJƒZ4dKZ5dLZ6i Z7da8dydNdO„Z9e
dPƒZ:g dQ¢Z;dRdS„ Z<dTdU„ Z=dVdW„ Z>dXdY„ Z?dZZ@eAƒ ZBdzd[d\„ZCd]d^„ ZDd_d`„ ZEdadb„ ZFdcdd„ ZGdedf„ ZHdgdh„ ZIdidj„ ZJdkdl„ ZKdmdn„ ZLdodp„ ZMdqdr„ ZNdsdt„ ZOdudv„ ZPeQdwk�rdePƒ  dS ){aô  
PAPER market maker for Kalshi soccer. Places NOTHING. Ever.

WHAT IT DOES. Every cycle it looks at fixtures inside the pre-kickoff window,
works out the quotes a market maker WOULD post, and writes them to a log. A
separate script (mm_settle.py) later checks Kalshi's real trade history to see
which of those quotes would have been hit, and what they settled for.

WHY IT EXISTS. research/mm_feasibility.py measured, on 637 settled events, that
the PASSIVE side of Kalshi soccer trades earns +30% in the first decile of a
market's volume and loses in the last, monotonically. Makers pay no fee; takers
pay 0.07*P*(1-P) (~4.4% of stake). So making is the structurally correct side.
The one thing history CANNOT answer is capture -- what share of those fills you
would actually win against whoever else is quoting. This logs the decisions so
that question can be answered forward without risking money.

THE RULES, all from measurement rather than taste:
  - WINDOW + TIME-STAGED MARGIN (2026-07-26 redesign). research/mm_decile_by_volume.py
    showed the passive-side edge is huge in a market's first ~30% of volume
    (t=+11.6, +7.2, +5.2 across the first three deciles) then fades to
    significantly NEGATIVE by the back half (t=-3.2 to -5.6). Separately, that
    first decile finishes at a MEDIAN of 2.7h before kickoff but a P90 of 4.7h --
    so a fixed 3h window missed the earliest-starting tenth of events entirely.
    Two changes follow directly from that:
      1. WINDOW_H widened to 5.0h to catch that P90 tail. Cost is negligible --
         events see essentially no volume before 4-5h out anyway, so this mostly
         means quoting into empty air a little earlier, not wasted risk.
      2. MARGIN now STAGES DOWN as kickoff approaches, via `margin_for(hrs)`,
         because we cannot observe "% of this event's eventual total volume" in
         real time (the total is unknown until the event ends) but we CAN
         observe hours-to-kickoff, which the historical data ties directly to
         volume decile:
           >= 2.5h  -> 5.0c   (the true "be first" phase; thin, high-edge)
           1.5-2.5h -> 3.0c   (still solidly positive, decile ~10-30%)
           0.5-1.5h -> 2.0c   (fading edge, decile ~30-50%; old flat default)
           <  0.5h  -> 1.0c   (decile 50%+ is where the historical edge turns
                               NEGATIVE for the passive side -- quote thin here
                               deliberately, mainly to allow complete_book() to
                               finish a book that's already 2/3 filled, not to
                               invite fresh exposure)
    These thresholds are read off the DECILE BOUNDARIES, not fitted to this
    project's own paper results (which remain far too few legs to fit anything
    to) -- but they are a first cut, not a proven schedule. Revisit once enough
    paper legs exist to check it directly.
  - REST, DON'T CHASE. Once a leg is posted, `cycle()` never rewrites its price
    in state -- the ask that decides whether a trade counts as a fill is always
    the ORIGINALLY posted one, even though a fresh (and possibly different)
    `our` is computed and logged every cycle for visibility. This was already
    true before today's redesign; it matters more now, because time-priority is
    the entire point of showing up early -- re-quoting down every cycle to
    chase the market would throw away exactly the priority this is meant to buy.
  - FAST, PERSISTENT POLLING. A 5-minute Task-Scheduler cron cannot be "first"
    against other makers who are plausibly resting continuously. Run as a
    persistent process (like live_monitor.py) with POLL_SECS in the tens of
    seconds, guarded by a restart task, rather than fired anew every 5 minutes.
  - NO SEPARATE OVERROUND GATE -- redundant once the ask is capped at the
    market's own ask (see below): our ask only gets hit when the market is
    already paying above our target, which is the condition the gate existed to
    detect. Kept only as logged fields (`over_mid`, `over_ask`) for later study.
  - CAP AT THE MARKET'S OWN ASK, FLOOR AT FAIR. The first version of this
    quoter asked a flat fair+2c with NO ceiling and sat above the market's own
    ask on 86% of legs (verified against the historical log) -- it essentially
    never had a chance to be lifted. `our = min(target, market_ask)`, and if the
    market's own ask is already below our fair estimate, we skip that leg
    entirely (selling below your own fair in isolation is guaranteed-negative
    EV, and a stale/bad Pinnacle read should not be able to force one).
  - LEVEL from PINNACLE, not from our own models. Elo and form features were
    measurably WORSE than the closing line (1.012 vs 0.985 log loss on 32k
    fixtures), and as a maker your fair value IS your protection -- quoting off
    a worse number just donates to anyone using a better one.

SAFETY: there is no live-order code path in this file at all. It imports no
trader module. The only way this places an order is if someone writes one.

Usage:  python main\mm_quoter.py [--once]
é    N)ÚPathzE:\soccer\main)Úbuild_event_tickerÚfind_event_by_abrsÚget_abrz&E:\soccer\research\mm_paper_quotes.csvz&E:\soccer\research\mm_paper_state.jsong      (@é   é
   T))g      "@ç      @)ç      @ç      @)r
   ç      @)ç      @ç       @)ç      ø?ç      ð?Ú ç     ÀX@c                 C   s`   d}t D ]\}}||kr|} q"qtt | › d|› � ¡ ¡ ¡ dƒd d }tdtd|| ƒƒS )aS  Premium in cents over the market ask, decaying toward the touch.

    A per-leg offset of -1/0/+1 (md5-stable) is kept so that at any given time
    different legs sit at different rungs. Without it, price and time-to-kickoff
    would be perfectly confounded and the fill curve could never be separated
    from a time-of-day effect.
    ç        ú|é   é   é   r   )ÚDECAYÚintÚhashlibZmd5ÚencodeZ	hexdigestÚmaxÚmin)ÚeventÚsideÚhrsÚbaseZcutÚvalZoff© r"   úE:\soccer\main\mm_quoter.pyÚarm_for¨   s    *r$   r   r   g      .@iô  i�  r   c                  C   sh   ddl } |   ¡ t dk rtS z(t ¡ }|tkatsBtd|› d�ƒ W n tyZ   daY n0 |   ¡ atS )zFalse when API-Football is nearly spent. Cached 5 min; fails OPEN so a
    status-endpoint hiccup never silently halts quoting.r   Né,  z  [quota] API-Football down to z -- pausing quotingT)ÚtimeÚ_quota_checked_atÚ	_quota_okÚLMZget_api_remainingÚAF_QUOTA_FLOORÚprintÚ	Exception)Ú_tZ	remainingr"   r"   r#   Úquota_okò   s    
r.   Zv4r   c              	   C   s.   zt t| ƒdƒW S  ttfy(   Y dS 0 d S )Nr   r   )ÚroundÚfloatÚ	TypeErrorÚ
ValueError)Úxr"   r"   r#   Ú_num  s    r4   zAmerica/Argentina/Buenos_AireszAmerica/Sao_PaulozAmerica/SantiagozAmerica/BogotazAmerica/LimazAmerica/Mexico_CityzAmerica/TorontozAmerica/New_Yorkz
Asia/SeoulzAsia/Shanghaiz
Asia/TokyozEurope/OslozEurope/StockholmzEurope/CopenhagenzEurope/WarsawzEurope/PraguezEurope/BucharestzEurope/ZurichzEurope/ZagrebzEurope/IstanbulzEurope/AthenszEurope/LondonzEurope/MadridzEurope/RomezEurope/BerlinzEurope/PariszEurope/AmsterdamzEurope/BrusselszEurope/Lisbon),ZAR1ZBSAZBR2ZCL1ZCO1ZPE1ZMX1ZCANZMLSZUSLZNWSLZKO1ZCN1ZJP1ZNO1ZSE1ZDK1ZPL1ZCZ1ZRO1ZCH1ZHR1ZT1ZG1ZE0ZE1ZE2ZE3ZECZSC0ZSC1ZSC2ZSC3ZSP1ZSP2ZI1ZI2ZD1ZD2ZF1ZF2ZN1ZB1ZP1c                 C   sd   t  | p
d ¡ ¡}|sdS zddlm} | ||ƒ¡}W n tyL   Y dS 0 t|j|j	d  dƒS )zDHour-of-day (float) in the league's own timezone, or '' if unmapped.r   r   )ÚZoneInfoç      N@é   )
Ú	LEAGUE_TZÚgetÚupperZzoneinfor5   Z
astimezoner,   r/   ZhourZminute)ÚdivÚnowÚzr5   Útr"   r"   r#   Ú_local_hour>  s    r?   c                 C   s&   t | ƒ}|r"t||  ¡ d dƒS dS )Nç      ¬@r7   r   )Ú_ptr/   Útotal_seconds)Ú	open_timer<   r>   r"   r"   r#   Ú_age_hK  s    rD   é<   c                 C   s”   | ||t |ƒf}t |¡}|r:t ¡ |d  tk r:|d S z.ddl}|jt |ƒ||t | ƒ ¡ d�pdi }W n ty~   i }Y n0 t ¡ |ft|< |S )zG{'home_bid':..,'home_ask':..,...} from Polymarket.com, or {} if absent.r   r   N)Zleague)	ÚstrÚ	_PC_CACHEr9   r&   Ú_PC_TTLZpolymarket_liveZget_all_pricesr:   r,   )r;   ÚhomeÚawayÚ	game_dateÚkeyZhitZ_plÚrr"   r"   r#   Úpoly_com_prices\  s    
&
rN   c                 C   s4   | dkrdS | dkrdS | dkr$dS | dkr0dS dS )	aA  Cents above fair to target, staged by hours-to-kickoff.

    Derived from research/mm_decile_by_volume.py's per-event decile results:
    decile 0-10% t=+11.58, 10-20% t=+7.16, 20-30% t=+5.20 (all strongly
    positive for the passive/maker side), fading through 30-50% (t=+1.49,
    +2.40), flat at 50-60% (t=+0.08), then SIGNIFICANTLY NEGATIVE from 60%
    onward (t=-1.54 to -5.57). First-decile timing: median 2.7h before
    kickoff, p90 4.7h. This maps volume deciles (unobservable live -- the
    event's eventual total volume isn't known until it's over) onto
    hours-to-kickoff (observable live) via that median timing.

    The >=5h stage was added with WINDOW_H 5->12: the 5-12h bucket is the
    RICHEST in the whole dataset at +15.85% maker ROI, so ask wider there --
    though it is only 2.1% of volume, so expect few fills, and treat the wide
    ask as "priced for how rarely anyone shows up", not as free money.

    FIRST CUT, not fitted to this project's own paper results -- there are
    nowhere near enough legs yet to fit anything to. Revisit once there are.
    r   r	   r   r   r   g      à?r   r   r"   )Z	hrs_to_kor"   r"   r#   Ú
margin_fork  s    rO   g     @@) Útsr;   rI   rJ   Úkickoffr   Ú
leg_tickerr   Úpin_fairÚk_bidÚk_askÚover_midÚover_askÚour_askÚcompetitiveÚsizeÚ
collateralÚreasonÚarmÚbid_sizeÚask_sizeÚvolumerC   Ú	mkt_age_hÚopen_interestÚ
volume_24hÚno_bidÚno_askÚ
local_hourÚregimeÚpc_bidÚpc_askc                  C   s4   dd l } z|  t ¡ ¡W S  ty.   i  Y S 0 d S )Nr   )ÚjsonÚloadsÚSTATEÚ	read_textr,   )rj   r"   r"   r#   Ú
load_state›  s
    rn   c                 C   s    dd l }t |j| dd�¡ d S )Nr   r   ©Úindent)rj   rl   Ú
write_textÚdumps)Ústrj   r"   r"   r#   Ú
save_state£  s    rt   z%E:\soccer\research\mm_gate_state.jsoné   r%   r"   c                 C   sb  ddl }�z>z| t ¡ ¡}W n ty4   i }Y n0 | d¡pBi }| ¡  ¡ }| |g g dœ¡}t	| d¡ppg ƒt	| d¡p€g ƒ }}	| D ]0}
|
d › d|
d	 › �}|
d
 r²|n|	 
|¡ qŒ|	|8 }	t|ƒt|	ƒ |d< |d< t|ƒdt … D ]}| |d¡ qòt |j| ¡ ttttt| dd„ d�t|dd„ d�|dœdd�¡ W n t�y\   Y n0 dS )aÃ  Snapshot of what the liquidity gate is doing to the books in window.

    `ahead` is DISPLAY ONLY and is deliberately kept out of the daily tallies:
    those have to keep meaning "books the gate actually ruled on that day", or
    the admitted-share stat would start counting books hours before they were
    ever quotable, and a book that was thin at 30h but fat by 10h would be
    tallied on the strength of a reading the gate never acted on.
    r   NÚdaily)ÚadmittedÚrejectedrw   rx   r;   r   ÚmatchÚpassc                 S   s   | d S ©Nr   r"   ©rM   r"   r"   r#   Ú<lambda>è  ó    z#_write_gate_state.<locals>.<lambda>)rL   c                 S   s   | d S r{   r"   r|   r"   r"   r#   r}   é  r~   )rP   Úgaterg   Zwindow_hZ
sweep_secsr<   Úaheadrv   r   ro   )rj   rk   Ú
GATE_STATErm   r,   r9   ÚdateÚ	isoformatÚ
setdefaultÚsetÚaddÚsortedÚGATE_KEEP_DAYSÚpoprq   rr   ÚMIN_LEG_VOLUMEÚREGIMEÚWINDOW_HÚGATE_SWEEP_SECS)Zrowsr<   r€   rj   Úprevrv   ÚtodayÚdZadmZrejrM   rL   Úkr"   r"   r#   Ú_write_gate_stateÇ  s>    	
&ø	÷
r’   z$E:\soccer\research\mm_live_fills.csv©Zreleased_atr   rR   r   rQ   rX   ÚfilledÚ	fill_kindÚfill_pxÚ	completedÚ	flattenedÚbook_closedÚqueue_aheadÚvol_cumÚvolume_at_postrg   c                 C   s:  �zt  ¡  }tt dddd��î}tj|td�}|r:| ¡  | tj	 
tjj¡ ¡ | dd¡| | dd¡| dd¡| d	d¡tt| d
¡ƒƒ| dd¡| dd¡tt| d¡ƒƒtt| d¡ƒƒtt| d¡ƒƒ| dd¡| dd¡| dd¡| dt¡dœ¡ W d  ƒ n1 �s0    Y  W n t�y4   Y n0 dS )añ  Record what the LIVE quoter believed about a leg, before its state is
    dropped.

    WHY THIS EXISTS. There are two incompatible definitions of "filled" in this
    system and only one of them was ever persisted:
      * the LIVE quoter uses paper_filled() -- queue-aware, a lower bound, and
        it is what actually drives complete_book()/flatten_stalled();
      * mm_settle uses fill_from_quotes() -- optimistic, its own docstring calls
        it a CEILING.
    Every number we report (completion rate, the P&L split by legs filled, the
    liquidity-gate sweep) comes from the settler, so we have been analysing the
    ceiling while the quoter acted on the lower bound. That gap is why the
    settler scores 59 books as 2-of-3 while the quoter took no action on 39 of
    them -- it never believed two legs had sold.
    Persisting the live view is the only way to measure the gap instead of
    guessing at it. Append-only; failures are swallowed so bookkeeping can never
    break quoting.
    Úar   úutf-8©ÚnewlineÚencoding©Z
fieldnamesr   r   rQ   rX   r”   r•   r–   r—   r˜   r™   rš   r›   rœ   rg   r“   N)Ú
LIVE_FILLSÚexistsÚopenÚcsvÚ
DictWriterÚLIVE_FILL_FIELDSÚwriteheaderÚwriterowÚdtÚdatetimer<   ÚtimezoneÚutcrƒ   r9   r   Úboolr‹   r,   )Zleg_keyÚvÚnewÚfhÚwr"   r"   r#   Ú_archive_state÷  s,    




ô,r´   c              	   C   sx   t | ƒD ]j}ztj | | d ¡}W n" tyF   |  |d¡ Y qY n0 ||  ¡ dkrt|| | ƒ |  |d¡ q| S )a  Free collateral once kickoff is >4h past: the match has finished and the
    paper position has settled. Archives the live view first -- see
    _archive_state; without that the quoter's own belief is lost forever and
    only the optimistic settler survives to be analysed.rQ   Ni@8  )Úlistr«   r¬   Úfromisoformatr,   r‰   rB   r´   )rs   r<   r‘   Úkor"   r"   r#   Úrelease_expired"  s    
r¸   c                 C   s   t dd„ |  ¡ D ƒƒS )Nc                 s   s   | ]}|  d d¡V  qdS )r[   r   N©r9   ©Ú.0r°   r"   r"   r#   Ú	<genexpr>4  r~   zcommitted.<locals>.<genexpr>)ÚsumÚvalues©rs   r"   r"   r#   Ú	committed3  s    rÀ   c                    s(   dd„ | D ƒ}t |ƒ‰ ‡ fdd„|D ƒS )Nc                 S   s   g | ]}d | ‘qS )r   r"   rº   r"   r"   r#   Ú
<listcomp>8  r~   zdevig.<locals>.<listcomp>c                    s   g | ]}|ˆ  ‘qS r"   r"   rº   ©Úsr"   r#   rÁ   :  r~   )r½   )ÚoÚimpr"   rÂ   r#   Údevig7  s    rÆ   Zactivec                 C   s^   d}| D ]P}|  d¡pd ¡ }|tkrd}|dvr|tvrt |¡ td|›d|› �ƒ q|S )aÁ  Kalshi's own view: are all three legs tradeable right now?

    Requiring `active` is not merely a guard, it is the precondition for what
    we are about to do -- a resting order cannot be placed on a market that is
    not active. Observed values are `active` (open), `inactive` (halted at
    kickoff) and `finalized` (settled); anything else is logged ONCE so a new
    status shows up in the output instead of silently stopping the quoter.
    TZstatusr   F)ZinactiveZ	finalizedr   z-  [status] unrecognised Kalshi market status z on )r9   ÚlowerÚ
_TRADEABLEÚ_seen_odd_statusr†   r+   )ÚlegsÚevent_tickerZokÚmrs   r"   r"   r#   Úkalshi_playableA  s    	
rÍ   c                 C   s<   z t  d| ddœ¡}| dg ¡W S  ty6   g  Y S 0 d S )Nz/marketsr   )rË   ÚlimitZmarkets)ÚktÚ_getr9   r,   )rË   rM   r"   r"   r#   Úlegs_forU  s
    rÑ   c           	      C   sž   t  | p
d ¡ ¡}|sd S t|ƒt|ƒ }}|r4|s8d S t||||ƒt||||ƒfD ]}t|ƒrT|  S qTzt|||ƒ}|r€|nd W S  ty˜   Y d S 0 d S )Nr   )ÚmapsZleague_kalshi_seriesr:   r   r   rÑ   r   r,   )	r;   rK   rI   rJ   ZseriesZhaÚaaÚevÚer"   r"   r#   Úresolve_event]  s     ÿ
rÖ   c                 C   sF   |   d¡d }| ¡ dv rdS |  d¡d dd… }| |¡rBdS d	S )
zÖhome / draw / away for a leg, from the ticker suffix.

    Team codes are 2-4 chars (KC, SJ, LAFC), so a fixed [:3] slice mis-assigns
    ~2% of legs -- strip the 7-char date prefix and use startswith instead.
    ú-éÿÿÿÿ)ZTIEZDRAWÚdrawr   é   NrI   rJ   )Úsplitr:   Ú
startswith)rR   rË   rI   rJ   ÚsuffixZmidr"   r"   r#   Úleg_sideo  s
    rÞ   c                 C   s   d|  d|   S )zIKalshi taker fee per contract, verified against real fills: 0.07*P*(1-P).gìQ¸…ë±?r   r"   )Zpricer"   r"   r#   Ú	taker_fee|  s    rß   c              	   C   s8   zt j t| ƒ dd¡¡W S  ttfy2   Y dS 0 dS )a¬  Parse to an aware datetime. NEVER compare timestamp strings directly --
    quote times carry +00:00, kickoffs carry -04:00, Kalshi trades carry Z, and
    lexicographic comparison of those three formats is not meaningful (this
    silently filtered out 100% of trades in an earlier version of this
    function, which is how the paper quoter reported a 0-completion day while
    genuinely believing every book was still open).ÚZz+00:00N)r«   r¬   r¶   rF   Úreplacer2   r1   rÂ   r"   r"   r#   rA   �  s    rA   c              
   C   sÜ  |du s|du rdS t |  d¡ƒp$|}g dd  }}}	|	dk rÒ|ddœ}
|rT||
d< zt d	|
¡}W n tyz   Y qÒY n0 | d
g ¡}||7 }|	d7 }	| d¡}|rÒ|s¬qÒt |d  d¡ƒ}|dur6||kr6qÒq6|}d}|D ]®}t | d¡ƒ}|du sÞ||  k �r|k �sqÞ qÞz8t| d¡�p&dƒ}t| d¡�pF| d¡�pFdƒ}W n ttf�yh   Y qÞY n0 ||d k�r€||7 }||krÞ|}qÞ|�r®t|  dd¡| dƒ| d< ||k�rÄ| 	¡ | d< |  dd¡|  dd¡kS )a$  QUEUE-AWARE fill test. Are we actually at the front of the queue yet?

    A print at our price is NOT our fill. In a FIFO book the buyer takes the size
    already resting at that price first, and at the touch that is 1,749-44,427
    contracts (measured on Kalshi). The old test ("did anyone print at or above
    our ask") therefore counted other makers' fills as ours, and did so in a
    DIRECTIONAL way: at +0c we join the touch behind a huge queue, while at
    +3c/+5c we are alone at our price. So it flattered exactly the cheap arms and
    left the expensive ones honest, which biased the whole price-ladder result.

    Measured on 2026-07-29 (research/queue_aware_fill.py): optimistic 48% of legs
    filled vs 40% queue-aware; at +0c specifically 100% vs 67%.

    THE MODEL: we are filled once cumulative volume traded at or above our ask,
    since we posted, EXCEEDS the depth that was resting at our price when we
    posted. Trades above our price count too -- the book walked up through us.

    `queue_ahead` is set at post time: the touch depth if we joined the touch,
    else 0 (nobody else is showing our price). It is deliberately PESSIMISTIC --
    makers ahead of us can cancel, which we cannot observe -- so this is a lower
    bound where the old test was an upper bound. Truth is between; logging both
    (`vol_cum`) lets that gap be measured later rather than guessed.

    Volume is accumulated against a WATERMARK so each cycle only counts trades it
    has not already seen. One API call per unfilled leg per cycle, same as before.
    NFÚvol_watermarkr   é   éÈ   )ÚtickerrÎ   Úcursorz/markets/tradesÚtradesr   rØ   Zcreated_timer   Zyes_price_dollarsZcount_fpÚcountç•Ö&è.>r›   r7   rš   )
rA   r9   rÏ   rÐ   r,   r0   r1   r2   r/   rƒ   )Z	state_legrR   rX   Zsince_dtZ
kickoff_dtr<   Zmarkrç   ræ   ZpagesÚpr�   ZtrZoldestZnewestZaddedr3   r>   ZpxZcntr"   r"   r#   Úpaper_filledŽ  sR    


"$

rë   c
                    sò  ‡ ‡fdd„|D ƒ}
t |
ƒdkr$dS ‡fdd„|
D ƒ}t |ƒdkrFdS |d }ˆ|  d¡sjˆ|  d¡rndS |	|  ¡ d	 tkr†dS || d }d|  k r¦dk s¬n dS ˆ| d
 }|| d t|ƒd  }|
D ]}dˆ| d< qÔdˆ| d< t|dƒˆ| d< t|dƒˆ| d< | ¡ ˆ| d< | | ¡ ||||	 ¡ ˆ |ˆ|  d¡t|dƒdtˆ|  d¡t	dˆ|  d¡› d|d›d|d›dt|ƒd d›d|d›d�dœ¡ t
d|› d|› d|› d ˆ|  d¡› d!|d›d"|d›d#|d›d�ƒ dS )$a,  A book stuck at 1-of-3 near kickoff: BUY BACK the one sold leg, go flat.

    WHY THIS EXISTS SEPARATELY FROM complete_book(). complete_book fires only at
    EXACTLY 2-of-3 -- with one leg sold and two unsold it does nothing, so the
    book walked into kickoff as a naked directional short. On 2026-07-29 that
    happened to EIGHT legs across both venues.

    WHY BUY BACK RATHER THAN COMPLETE. Completing a 1-of-3 book needs TWO
    crossings (~4.6c); buying back needs ONE (~2.3c) and leaves zero exposure.

    WHY AT ALL, given naked legs are roughly break-even. Measured (Monte Carlo on
    the 2026-07-29 legs, n=200k): mean -0.04, MEDIAN +5.30, 1st pct -34.70. Mean
    ~zero because we sell at fair, but a long left tail with a positive median --
    the shape that reads as edge for weeks then gives it back. Worse, it is
    unmeasurable: detecting even a $0.50/day naked edge against $12.34 daily
    noise needs ~2,400 days. And it BURIES the signal we actually care about --
    on 2026-07-29 complete books made -$0.22 while naked legs swung +$7.50, 34x
    the noise. So this trades a small certain fee for the ability to measure
    whether market making works at all.

    Honest cost: buy-back is slightly NEGATIVE EV if we truly sell above fair.
    That is accepted deliberately, not overlooked.
    c                    s*   g | ]"}|ˆv rˆ|   d ¡ˆ kr|‘qS ©r   r¹   ©r»   r>   ©r   rs   r"   r#   rÁ     r~   z#flatten_stalled.<locals>.<listcomp>r   r   c                    s   g | ]}ˆ |   d ¡r|‘qS ©r”   r¹   rí   r¿   r"   r#   rÁ     r~   r   r˜   r—   r@   rX   éd   Tr™   r7   Z
flatten_pxZflatten_cost_cZflattened_atr   r]   zFLATTEN_1OF3: bought back z sold @ú.2fz at ask z, fee z
c => cost ú+.1fz
c/contract)rP   r;   rI   rJ   rQ   r   rR   r   rX   rY   rZ   r]   rg   r\   z    [FLATTEN] ú ú vs z: bought back z @z (sold z) -> )Úlenr9   rB   ÚCOMPLETE_BEFORE_KO_Hrß   r/   rƒ   rª   ÚSIZEr‹   r+   )rs   r   rÊ   ÚbookÚwriterr<   r;   rI   rJ   r·   Úminer”   ÚtgtZaskZsoldZcost_cr-   r"   rî   r#   Úflatten_stalledê  s`    
ÿÿÿþú	ÿÿÿþ
rü   c
                    sB  ‡ ‡fdd„|D ƒ}
t |
ƒdkr$dS t‡fdd„|
D ƒƒr>dS ‡fdd„|
D ƒ}t |ƒdkr`dS |d }ˆ|  d	¡rzdS |	|  ¡ d
 }|tkr–dS || d }d|  k r¶dk s¼n dS t‡fdd„|
D ƒƒ| }|d d t|ƒd  }dˆ| d	< dˆ| d< tˆ|  d¡ƒ}| ¡ ˆ| d< |�r@t	||  ¡ dƒndˆ| d< d	ˆ| d< |ˆ| d< dˆ| d< | 
| ¡ ||||	 ¡ ˆ |ˆ|  dd¡ˆ|  dd¡||| d ddt	|dƒdtt	d| t dƒd|d d›dt|ƒd d›d|d ›d!�d"œ¡ td#|› d$|› d%|› d&ˆ|  d¡› d'|d(›d)|d ›d!�ƒ dS )*a1  If exactly one leg of a 3-leg book is still unfilled, SELL it into the bid.

    WHY. A complete book is riskless: three asks each ~2c above fair sum to
    ~1.06, so you collect ~106c against a fixed 100c payout and the result does
    not matter. An INCOMPLETE book is a naked directional short. Measured on the
    first six paper matches: four complete books all landed near +7c/contract,
    while the two incomplete ones lost 48-50c. Completing the Korean books at the
    bid would have turned -$6.90 into +$4.26.

    Cost of completing is one taker fee (~1.7c at 57c) plus giving up the
    bid-ask. That is cheap against a 50c tail -- the opposite conclusion to
    "hedging costs 1.5c against a 2.5c edge", which measured the fee against the
    per-leg edge instead of against the risk being removed.
    c                    s*   g | ]"}|ˆv rˆ|   d ¡ˆ kr|‘qS rì   r¹   rí   rî   r"   r#   rÁ   ;  r~   z!complete_book.<locals>.<listcomp>r   r   c                 3   s   | ]}ˆ |   d ¡V  qdS )r™   Nr¹   rí   r¿   r"   r#   r¼   >  r~   z complete_book.<locals>.<genexpr>c                    s   g | ]}ˆ |   d ¡s|‘qS rï   r¹   rí   r¿   r"   r#   rÁ   @  r~   r   r—   r@   c                 3   s(   | ] }ˆ |   d ¡rˆ | d V  qdS )r”   rX   Nr¹   rí   r¿   r"   r#   r¼   ^  r~   r   rð   Tr”   Ú	posted_atÚ	filled_atr   Ú	fill_secsr•   r–   Zcompleted_at_bidÚactionr   rS   r7   z COMPLETE_AT_BID: book collected ú.0fzc, fee ú.1fzc => rò   zc/contract locked)rP   r;   rI   rJ   rQ   r   rR   r   rS   rT   rU   rV   rW   rX   rY   rZ   r[   r\   z    [COMPLETE] ró   rô   z: sold z at bid rñ   z -> )rõ   Úanyr9   rB   rö   r½   rß   rA   rƒ   r/   rª   r÷   r+   )rs   r   rÊ   rø   rù   r<   r;   rI   rJ   r·   rú   Zunfilledrû   Zhrs_leftZbidZ	collectedZnet_cZ_pr"   rî   r#   Úcomplete_book,  s^    $
ÿÿø
&ÿÿ
r  c                 C   sš   z€|   d¡ptj ¡ }t|tƒr,tj |¡}t| d ƒ d¡dd… \}}tj|j	|j
|jt|ƒt|ƒt tjdd�¡d�|fW S  ty”   Y d	S 0 dS )
a  (kickoff_dt, game_date) for a watchlist row, or (None, None).

    Watchlist kickoff is HH:MM ET on the game date. Factored out of cycle() so
    the in-window quoting pass and the out-of-window telemetry sweep cannot
    drift apart on how they read a fixture's time.
    rK   rQ   ú:Nr7   éüÿÿÿ)Zhours)Ztzinfo)NN)r9   r«   r‚   r�   Ú
isinstancerF   r¶   rÛ   r¬   ZyearZmonthZdayr   r­   Z	timedeltar,   )rÌ   ÚgdZhhZmmr"   r"   r#   Ú_ko_ofw  s    
ÿÿr	  c                 C   st   t | || d¡| d¡ƒ}|s"dS t|ƒ}t|ƒdks@t||ƒsDdS dd„ |D ƒ}|r^t|ƒndtdd	„ |D ƒƒfS )
z’(minvol, sorted vols) for one fixture on Kalshi, or None if it is not a
    playable 3-leg book there. Exactly the reads the in-window pass makes.rI   rJ   Nr   c                 S   s   g | ]}t | d ¡pdƒ‘qS ©Ú	volume_fpr   ©r0   r9   ©r»   ÚLr"   r"   r#   rÁ   ’  r~   z_gate_probe.<locals>.<listcomp>r   c                 s   s   | ]}t |d ƒV  qdS ©r   N©r/   rº   r"   r"   r#   r¼   “  r~   z_gate_probe.<locals>.<genexpr>)rÖ   r9   rÑ   rõ   rÍ   r   r‡   )r;   r  rÌ   rÔ   rÊ   Úvolsr"   r"   r#   Ú_gate_probe‰  s    r  c                 C   sv  t   ¡ t tk}g tƒ  }}| D �] }t|ƒ\}}|du r<q ||  ¡ d }|tkrVq | d¡pbd ¡ }	|	› d| dd¡› d| d	d¡› �}
| 	|
¡ |s¢|
t
vrÚzt|	||ƒ}W n tyÈ   d}Y n0 t   ¡ |ft
|
< t
|
 \}}|du rðq |\}}| |	|
 dd
¡d
 t|dƒt|d
ƒ||tkdtt   ¡ | d d
ƒdœ¡ q tt
ƒ| D ]}t
 |d¡ �qP|�rrt   ¡ a|S )aC  Gate telemetry for watchlist books still OUTSIDE the quoting window.

    Volumes refresh at most every GATE_SWEEP_SECS and hours-to-kickoff is
    recomputed every call, so a row ages honestly rather than freezing. Returns
    display rows only -- see _write_gate_state on why these stay out of the
    daily tallies.
    Nr@   Úlcr   r   rI   ú?rô   rJ   r   r7   r€   r6   )r;   ry   r   Úminvolr  rz   ÚstageZ	vol_age_m)r&   Ú_ahead_swept_atr�   r…   r	  rB   rŒ   r9   r:   r†   Ú_ahead_cacher  r,   ÚappendrÛ   r/   rŠ   r‰   )Úmatchesr<   ZdueÚoutÚseenrÌ   r·   r  r   r;   rL   ZprobeZ	probed_atr  r  r‘   r"   r"   r#   Ú_ahead_rows–  sB    	
$

ü
r  c           5         s	  t ƒ s
dS t ¡ \}}tj tjj¡}tt	ƒ |ƒ}d}d}g }|D �]®}	t
|	ƒ\}
}|
d u r^qB|
|  ¡ d }d|  k r‚tks†qB qB|	 d¡p’d ¡ }t|||	d |	d ƒ}|s´qBt|ƒ}t|ƒdkrÊqBt||ƒsÖqBd	d
„ |D ƒ}|rðt|ƒnd}||	 dd¡› d|	 dd¡› �t|dƒt|dƒtdd„ |D ƒƒ|tkddœ}| |¡ |tk �rVqBtj|	 d¡tjd��spqBi }i }i ‰|D �]‰ z,tˆ  d¡�p˜dƒ}tˆ  d¡�p¬dƒ}W n  ttf�yÒ   d }}Y n0 d|  k �rø|  k �rødk �sn i } �q˜||f|ˆ d < tˆ  d¡ƒtˆ  d¡ƒtˆ  d¡ƒf|ˆ d < ˆ  d¡�pRdtˆ  d¡ƒtˆ  d¡ƒˆ  d¡�pxdˆ  d ¡�p†dd!œˆˆ d < �q€|�s qBtd"d„ | ¡ D ƒƒ}td#d„ | ¡ D ƒƒ}td$d„ | ¡ D ƒƒ}|| d% t k�röqBt|d& d% dƒ|d'< |d& d% t!k �r(d(|d)< qBd*|d)< t "|	 d¡¡\}‰ˆrBt#‡fd+d„d,D ƒƒ�sbqBt$ˆd- ˆd. ˆd/ gƒ}t%|d |d |d d0�}t&||	d |	d |ƒ}|D �]‰ t'ˆ d ||	d |	d ƒ}t(|||ƒ}|ˆ d  \} }!|| d% }"|"t)|ƒ }#t|!d% d1ƒt*�r|nd }$tt|#|$ƒd1ƒ}%|%|"t+ k }&|&�sPt, -|%¡d2 nd }'|& �op|'d u�op|'|!d3 k}(|&�r†t, -|"¡d2 }'d&|' t. })ˆ d |v }*t/t0|ƒ }+|*�p¶|)|+k},d}-|&�rúd4|!d5›d6|"|!d%  d7›d8|| d9›d:t+d7›d;�	}-d<}(n |,�sd=|)d5›d>|+d5›d?�}-d<}(|*�r.|ˆ d   d@¡�s.|'d u�r.|ˆ d  dA }.|t1k�r\|!n|'}/t, -t|/d% d1ƒ¡d2 }/|/|.d3 k �r.|/|ˆ d  dA< |/|!d3 k�rÂt| ˆ d dB¡d �p¾dƒnd|ˆ d  dC< d|ˆ d  dD< | 2¡ |ˆ d  dE< | 2¡ |ˆ d  dF< d|ˆ d   dGd¡ |ˆ d  dG< |,�r²|*�s²|&�s²t|)dƒ|
 2¡ |'||t|| dHƒd<d<t3|(ƒ|t4|'|!d3 k�ršt| ˆ d dB¡d �p–dƒndd| 2¡ dIœ|ˆ d < |  5| 2¡ ||	d |	d |
 2¡ |ˆ d |t|| dHƒ| |!t|dHƒt|dHƒt|'dƒt6|(ƒt.td&|' t. dƒ|-|| ˆ d dJ¡d | ˆ d dJ¡d | ˆ d dJ¡d dKœ‡ ‡fdLdM„d!D ƒ¥t7ˆ ˆ d i ¡ d¡|ƒt8||ƒt4| |› dN�d¡| |› dO�d¡dPœ¥¡ ||(�rÆdnd7 }�q°t9|ƒD ]°}0| |0¡}1|1�rØ|1 d@¡�sØ|1 dQ¡�r�qØt:|1 dR¡ƒ}2t:|1 dS¡ƒ}3|2�rØ|3�rØt;|1|0|1dA |2|3|ƒ�rØdT|1d@< |1dA |1dU< | 2¡ |1dV< t||2  ¡ dƒ|1dW< dX|1dY< �qØ|�oœdZd
„ |D ƒ}4|t<|||4|| |||	d |	d |
ƒ
7 }|t=|||4|| |||	d |	d |
ƒ
7 }| >¡  qBt?||t@||ƒƒ tA|ƒ ||fS )[N)r   r   r   r@   r  r   rI   rJ   r   c                 S   s   g | ]}t | d ¡pdƒ‘qS r
  r  )r»   rÌ   r"   r"   r#   rÁ   æ  r~   zcycle.<locals>.<listcomp>r   r  rô   r7   r   c                 s   s   | ]}t |d ƒV  qdS r  r  rº   r"   r"   r#   r¼   ê  r~   zcycle.<locals>.<genexpr>r   )r;   ry   r   r  r  rz   r  Zfid)Zmax_ageZyes_bid_dollarsZyes_ask_dollarsrå   Zyes_bid_size_fpZyes_ask_size_fpr  rC   Zopen_interest_fpZvolume_24h_fpZno_bid_dollarsZno_ask_dollars)rC   rb   rc   rd   re   c                 s   s   | ]\}}|| d  V  qdS )r7   Nr"   ©r»   Úbr�   r"   r"   r#   r¼     r~   c                 s   s   | ]\}}|V  qd S ©Nr"   r  r"   r"   r#   r¼     r~   c                 s   s   | ]\}}|V  qd S r   r"   r  r"   r"   r#   r¼     r~   rð   r   Zgross_cZgross_rejectr  Úquotedc                 3   s   | ]}ˆ   |¡V  qd S r   r¹   ©r»   r‘   )Úpinr"   r#   r¼   J  r~   )Úhr�   r�   r$  r�   r�   )rI   rÙ   rJ   é   g      Y@ré   zmarket ask rñ   z is r  zc below our fair z.3fz (> z3c tolerance) -- our number is likely stale, skippedFzno collateral: need $ú, $z freer”   rX   )r   r   r   rš   r›   râ   Zrepriced_atZreprice_counté   )r[   rQ   rX   r   r   rS   r”   r—   rY   r]   rg   rš   r›   rý   )r   r   r   )rP   r;   rI   rJ   rQ   r   rR   r   rS   rT   rU   rV   rW   rX   rY   rZ   r[   r\   r]   r^   r_   r`   c                    s&   i | ]}|ˆ  ˆ d  i ¡  |d¡“qS )rå   r   r¹   r"  )r  Úextrar"   r#   Ú
<dictcomp>æ  s   ÿzcycle.<locals>.<dictcomp>Z_bidZ_ask)ra   rf   rg   rh   ri   r™   rý   rQ   Tr–   rþ   rÿ   Znaturalr•   c                 S   s   g | ]}|d  ‘qS )rå   r"   r  r"   r"   r#   rÁ     r~   )Br.   r)   Zload_watchlist_filer«   r¬   r<   r­   r®   r¸   rn   r	  rB   rŒ   r9   r:   rÖ   rÑ   rõ   rÍ   r   r/   r‡   rŠ   r  Zfixture_playableZPLAYABLE_BACKSTOP_TTLr0   r1   r2   r4   r½   r¾   ÚMAX_SPREAD_CÚGROSS_GATE_CZfetch_prematch_oddsÚallrÆ   ÚdictrN   rÞ   r$   rO   Ú
AB_ENABLEDÚFAIR_TOLERANCE_CÚmathZceilr÷   ÚBANKROLLrÀ   rö   rƒ   r¯   r‹   rª   r   rD   r?   rµ   rA   rë   r  rü   Úflushr’   r  rt   )5rù   r²   Ú_r  r<   rs   r!  r—   Z	gate_rowsrÌ   r·   r  r   r;   rÔ   rÊ   r  r  Zgrowrø   Zdepthr  r�   rV   rW   Zover_bidZb365ZfairZfmapZpcr   r]   ZbbrÓ   Zfair_cZtarget_cZ	ceiling_cZour_cZskip_below_fairZourZcompZneedÚalreadyZavailZfundedr\   Zcurrû   r>   rÃ   ZpostedZleg_koZ_tkr"   )r  r(  r#  r#   ÚcycleÁ  s|   


ý


&þû
ÿÿ!$
ÿ"þÿ
þÿÿÿñõÿô
ï

ÿÿÿ
r5  c               
   C   s
  dt jv } t ¡  }ttdddd�}tj|td�}|r>| ¡  t	dt
› dt› d	td
›dttƒ ƒd›d�	ƒ zBt||ƒ\}}t	tj ¡ d›d|› d�|r¢d|› d�nd ƒ W n> tyê } z&t	tj ¡ d›d|› �ƒ W Y d }~n
d }~0 0 | ròqþt t¡ qj| ¡  d S )Nz--oncer�   r   rž   rŸ   r¢   zPAPER quoter running (window z/h, staged margin 6/5/3/2/1c by hrs-to-KO, size z, bankroll $r  r&  rñ   z committed). Places nothing.z%H:%M:%Sz  logged quotes on z legsz, completed z book(s)z  cycle error: )ÚsysÚargvÚOUTr¤   r¥   r¦   r§   ÚFIELDSr©   r+   rŒ   r÷   r1  rÀ   rn   r5  r«   r¬   r<   r,   r&   ÚsleepÚ	POLL_SECSÚclose)Úoncer±   r²   r³   ÚnÚcrÕ   r"   r"   r#   Úmain  s0    


ÿÿ
þ
ÿ
0r@  Ú__main__)r   r   )r"   )r   )RÚ__doc__r6  Úosr¦   r&   r0  r   r¬   r«   Úpathlibr   ÚpathÚinsertrÒ   Zkalshi_traderrÏ   Zkalshi_liver   r   r   Zlive_monitorr)   r8  rl   rŒ   r;  r÷   r.  r   r$   rö   r+  r*  rŠ   r*   r'   r(   r.   r‹   r/  r4   r8   r?   rD   rG   rH   rN   rO   r1  r9  rn   rt   r�   rˆ   r�   r  r  r’   r£   r¨   r´   r¸   rÀ   rÆ   rÈ   r…   rÉ   rÍ   rÑ   rÖ   rÞ   rß   rA   rë   rü   r  r	  r  r  r5  r@  Ú__name__r"   r"   r"   r#   Ú<module>   s¶   L!
ð&
)+
\BK+  P
