a
    5°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 ddlm	Z	 dZ
dZdZdZg d	¢Zd
d„ Zdd„ Zdd„ ZdZdZdZdd„ Zdd„ Zdd„ Zdd„ Zdd„ Zdddd„Zdd d!œZd"d#„ Zd$d%„ ZzPddlZddlZ ej! "de j! #e j! $e j! $e j! %e&¡¡¡d&¡¡ dd'l'm(Z( W n e)�y8   d(d)hZ(Y n0 d*d+„ Z*d,d-„ Z+d.d/„ Z,e-h d0£ƒZ.d1Z/d2Z0e-e.d3d4h d5d6hB ƒZ1d7d8„ Z2e2Z3d9d:„ Z4d;d<„ Z5d=Z6d>d?„ Z7d@Z8dAZ9dBZ:e9e:fdCdD„Z;e9e:fdEdF„Z<dGdH„ Z=dIdJ„ Z>dK\Z?Z@dLdM„ ZAdNdO„ ZBdPdQ„ ZCdRdS„ ZDdTdU„ ZEdVZFdWZGdXdY„ ZHdZd[„ ZId\d]„ ZJd^d_„ ZKded`da„ZLdbd&„ ZMeNdck�rXeMƒ  dS )fun  
FORWARD TEST (read-only, trades nothing): the cross-book "exclusive-low" rule
(PRIMARY) plus the older "unique vs overlap" baseline, on out-of-sample fixtures.

PRIMARY RULE â€” "exclusive low", STRICT (user's formulation; spec corrected
2026-07-05 â€” see fire_xlow docstring for the correction history):
  bet the CHEAPER book's side when (1) its mid is BELOW the other book, (2) its
  pin-CLV is in [0.05,0.12), AND (3) the other book is NOT in band â€” one book in
  band while the other sits near Pinnacle, confirming the fair.
  Backtest on poly_backfill (975 shared-price fixtures):
    plain Kalshi pin-CLV [5,12)        n=421  +11.0%  z+1.26  p=0.105   (current live)
    exclusive-low STRICT [5,12)        n=286  +24.7%  z+2.59  p=0.005
      -> OOS TAIL (last 40%) +43.7% z+2.81
  (Under below-other, "other not in band" == "other CLV < 5%" â€” the cheaper book
  always carries the higher CLV, so the formulations coincide; verified bet-for-
  bet.) Still pin-anchored (pure two-book deviation with no Pinnacle is dead â€”
  see project_pure_xbook_deviation).

SECONDARY â€” "unique vs overlap" (2026-06-28): bets where exactly ONE book fires
  [0.05,0.12) ("unique") beat bets where BOTH fire ("overlap"): full UNIQUE n=117
  +61.9% z+3.52 vs OVERLAP n=267 +10.8% z+1.51; held across OOS splits. The exclusive-
  low rule is the cleaner, stronger productionizable form of the same edge â€” kept here
  for continuity.

This FREEZES the rules as of FREEZE_DATE and tracks them on LATER live_data fixtures
for clean OOS. NOTHING traded.

Selection (NON-look-ahead, mirrors live): per book, first pre-KO snapshot whose mid-CLV
(multiplicative pin_fair) qualifies. Kalshi settles @ ask; Poly @ real poly_*_ask when
present (live_data) else est-ask (mid + near-term half-spread, for backfill). One
bet/fixture (max CLV). Append one row/run.

BLOCKER (honest): needs live_data rows carrying BOTH kalshi_* AND poly_* prices. The
live Poly feed has been dark since 2026-06-18 (club off-season) and live_monitor must
be RESTARTED with Poly wired for poly_* to populate. Until then OOS shared-price
fixtures = 0 and this logs n=0 -- expected, not a bug.
é    N)ÚdatetimeÚdateÚ	timedelta)ÚdefaultdictzE:\soccer\live_dataz3E:\soccer\research\forward_test_unique_book_log.csvz
2026-07-05)çš™™™™™©?g¸…ëQ¸¾?)z%Y-%m-%d %H:%M:%Sz%m/%d/%Y %H:%Mz%m/%d/%Y %H:%M:%Sz%Y-%m-%d %H:%Mc              
   C   s8   t D ].}zt | |¡W   S  ttfy0   Y q0 qd S ©N)Ú_FMTSr   ÚstrptimeÚ
ValueErrorÚ	TypeError)ÚsÚf© r   ú$research/forward_test_unique_book.pyÚ_pts0   s    r   c              	   C   s(   z
t | ƒW S  ttfy"   Y d S 0 d S r   )Úfloatr   r
   ©Úxr   r   r   Ú_f5   s    r   c                 C   s8   t | ƒD ]*}| d¡pd ¡  ¡ }|dv r|  S qd S )NÚftrÚ )ÚHÚDÚA)ÚreversedÚgetÚupperÚstrip)ÚrowsÚrÚvr   r   r   Ú_result8   s    r!   )ÚkalshiÚpolyusÚpolyg
×£p=
ï?g¸…ëQ¸ž?c                    s¬   t | ƒD ]ž‰ˆ d¡pd ¡ dkr$qtD ]|‰ ‡ ‡fdd„dD ƒ}tdd„ |D ƒƒrTq(d	d„ t|ƒD ƒ}d
d„ t|ƒD ƒ}t|ƒdkr(t|ƒdkr(d|d      S q(qdS )a  Result inferred from a settled market, or None. DISPLAY ONLY.

    `ftr` is backfilled from API-Football, so a match that finishes at 15:00
    has no result until that backfill runs -- and when the API quota is spent,
    not until the next day. The dashboard then shows finished matches as "open"
    for hours, which is what it looked like on 2026-08-15.

    The result is however already sitting in the same row: once a market settles
    the winning leg goes to ~1.00 and the others to ~0.00, which is exactly how
    resolve_bets derives its "D via kalshi". Requires status FT AND an
    unambiguous split, so an in-play blowout at 85' cannot be mistaken for a
    settled book.

    Deliberately NOT used by the scoring paths: results that a strategy is
    graded against must come from the match record, never from the prices that
    same strategy trades. This exists so a human can see a finished game as
    finished, and `load_fixtures` tags it ftr_src='market' so it is never
    mistaken for the real thing.
    Ústatusr   ZFTc              	      s&   g | ]}t ˆ ˆ › d |› d�¡ƒ‘qS )Ú_Ú_mid)r   r   )Ú.0r   ©Úbookr   r   r   Ú
<listcomp>]   ó    z'_result_from_market.<locals>.<listcomp>)ÚhomeZdrawÚawayc                 s   s   | ]}|d u V  qd S r   r   )r(   r    r   r   r   Ú	<genexpr>^   r,   z&_result_from_market.<locals>.<genexpr>c                 S   s   g | ]\}}|t kr|‘qS r   )Ú
_SETTLE_HI©r(   Úir    r   r   r   r+   `   r,   c                 S   s   g | ]\}}|t kr|‘qS r   )Ú
_SETTLE_LOr1   r   r   r   r+   a   r,   é   é   ZHDAr   N)r   r   r   Ú_SETTLE_BOOKSÚanyÚ	enumerateÚlen)r   ZlegsÚhiÚlor   r)   r   Ú_result_from_marketE   s    r<   c                    s(   dd„ | D ƒ}t |ƒ‰ ‡ fdd„|D ƒS )Nc                 S   s   g | ]}d | ‘qS )g      ð?r   )r(   r   r   r   r   r+   f   r,   zfair_mult.<locals>.<listcomp>c                    s   g | ]}|ˆ  ‘qS r   r   )r(   Zri©r   r   r   r+   f   r,   )Úsum)Úor   r   r=   r   Ú	fair_multe   s    r@   c                 C   s(   | dk rdS | dk rdS | dk r$dS dS )Nr   gú~j¼t“h?g      Ð?g{®Gázt?gffffffÖ?gš™™™™™™?g{®Gáz”?r   )Úmidr   r   r   Úhalf_spreadg   s    rB   c                 C   s  |dkr\t |  d|› d�¡ƒ}t |  d|› d�¡ƒ}||rVd|  k rNdk rVn n|n|fS |dvrrtd|›�ƒ‚t |  |› d	|› d�¡ƒ}|d
u r®t |  |› d	|› d�¡ƒ}t |  |› d	|› d�¡ƒ}|râd|  k ràdk sún |d
urö|t|ƒ nd
}||fS )u  (mid, ask) for a book/side. Works on live_data (poly_*_mid/ask) AND backfill
    (poly_*_price, no ask -> est-ask). Kalshi: mid + real ask.

    2026-07-27: `book` is now actually honoured for the non-Kalshi branch. It
    previously fell through to poly_* for ANY value, so book_quote(r,'polyus',..)
    silently returned Polymarket.COM prices â€” a wrong-but-plausible read of
    exactly the kind that has bitten this project before. Prefix is now derived
    from `book`, and an unknown book raises rather than guessing.r"   Zkalshi_r'   Z_askr   r4   )r$   r#   zbook_quote: unknown book r&   NZ_price)r   r   r
   rB   )r   r*   ÚsideÚmÚar   r   r   Ú
book_quotem   s    	,(rF   c              
   C   s¤   | D ]š}t | d¡ƒ}| d¡}|du s|dv r2qz|tt|ƒd� }W n ttfyb   Y qY n0 |jddddd�}t||  ¡ d ƒd	 }|t|d
�   S dS )a  Kickoff for a fixture, derived from any pre-match snapshot, or None.

    live_data carries no kickoff column, but `elapsed` is minutes relative to
    kickoff (negative before it), so KO = timestamp - elapsed falls out of any
    row. `elapsed` is whole minutes while the timestamp carries seconds, which
    lands the answer up to a minute early -- 05:59 for an 06:00 kick. Real
    kickoffs sit on 5-minute boundaries (:00 :05 :15 :30 :45), so rounding to
    the nearest 5 removes the artifact exactly rather than papering over it.
    Ú	timestampÚelapsedN)Nr   )Zminutesr   )ZhourZminuteÚsecondZmicrosecondg     Àr@i,  )Zseconds)	r   r   r   r   r   r
   ÚreplaceÚroundZtotal_seconds)r   r   ÚtsÚelÚtZmidnightZsecsr   r   r   Ú_ko_fromƒ   s    


rO   Tc                 C   sh  t tƒ}t tj td¡¡D ]p}t|ddd��N}t 	|¡D ]2}|| 
d¡| 
d¡| 
d¡| 
d¡f  |¡ q:W d	  ƒ q1 s‚0    Y  qg }| ¡ D ]¸\\}}}}	}
|s°qštd
d„ |
D ƒdd„ d�}|sÐqšt|
ƒ}|d	u ræ| ræqš||rðdnd	 }}|d	u �r| �st|
ƒ}|�rdnd	}| t|||||tdd„ |D ƒƒdd„ |D ƒd�¡ qš|jdd„ d� |S )ao  Fixtures keyed to their pre-kickoff snapshots.

    require_result=True (the default, and what every SCORING path uses) drops
    fixtures with no final result: a bet that cannot be settled is not evidence
    and must never reach stat().

    Pass False to see matches that have not been played yet -- the dashboard's
    live xhigh panel does, because "what has the rule taken on tonight's games"
    is exactly the question it answers. Those rows have ftr=None, and a caller
    showing them MUST render that as open rather than letting the `won` flag
    (which reads False on an unplayed fixture) print as a loss.
    z*.csvr   úutf-8©ÚnewlineÚencodingZ	game_dateÚleaguer-   r.   Nc                 S   s>   g | ]6}|  d ¡pd ¡ dkrt|d ƒrt|d ƒ|f‘qS )r%   r   ZPRErG   )r   r   r   )r(   r   r   r   r   r+   ²   s   "ÿz!load_fixtures.<locals>.<listcomp>c                 S   s   | d S )Nr   r   )rN   r   r   r   Ú<lambda>´   r,   zload_fixtures.<locals>.<lambda>©ÚkeyZapiZmarketc                 s   s   | ]\}}|V  qd S r   r   ©r(   r&   r   r   r   r   r/   Æ   r,   z load_fixtures.<locals>.<genexpr>c                 S   s   g | ]\}}|‘qS r   r   rX   r   r   r   r+   Ç   r,   )ÚgdrT   r   Úftr_displayÚftr_srcÚkoÚprec                 S   s   | d S )NrY   r   r   r   r   r   rU   È   r,   )r   ÚlistÚglobÚosÚpathÚjoinÚLIVE_DIRÚopenÚcsvÚ
DictReaderr   ÚappendÚitemsÚsortedr!   r<   ÚdictrO   Úsort)Zrequire_resultZfixturesra   Úfhr   ÚpreppedrY   rT   r-   r.   r   r]   r   rZ   r[   r   r   r   Úload_fixturesœ   s:    ÿÿ&þ	ý
rn   r5   ©r-   r.   c                 C   s†  | d D �]v}t | d¡ƒt | d¡ƒt | d¡ƒ  }}}d|||fv s|dkr|dkr|dksdqt|||fƒ}g }t ¡ D �]æ\}}t|d|ƒ\}	}
t|d|ƒ\}}|	du s~|du s~d	|	  k rÌdk r~n q~d	|  k rädk sèq~ q~|	|k�r|	|
|df\}}}}n|||	df\}}}}|du s~d	|  k �r<dk �s@q~ q~|| | ||  }|| | ||  }||k r~td	 |  k�r�td k �s”q~ q~td	 |  k�r´td k �rºq~ nq~| d
 dk�rÒ|dk�pæ| d
 dk�oæ|dk}| t||||||||| | d¡| d |  d¡| d | d¡| d¡| d
 |  d¡|  d¡|dk�rRdndk|  d¡d�¡ q~|rt	|dd„ d�  S qdS )u  Cheaper book BELOW the other book AND below Pinnacle (pin-CLV in BAND),
    AND the other book NOT in band â€” one book in band while the other isn't.

    SPEC CORRECTION 2026-07-05 (re-freeze): the user's original formulation was
    always the strict version; the loose 'below-other-only' variant wired on
    06-30 let half-cent mid gaps qualify when BOTH books were cheap (Gwangju
    07-05: Kalshi mid 0.120 vs Poly 0.125, both ~7% under fair, identical asks
    â€” an overlap case in xlow clothing). Strict backtest: [5,12) n=286 +24.7%
    z+2.59 (vs loose n=313 +23.1% z+2.87 â€” statistically equivalent; this is
    intent-alignment, not results-chasing: correction disclosed same day as a
    1W-6L shadow day, and strict loses that day identically 1W-6L â€” Gwangju
    still qualified via a LATER snapshot where Poly was the exclusive low.
    Known open nuance: on a 1c grid with flip-flopping books, first-qualifying-
    snapshot scanning finds transient strict configurations; a persistence
    requirement (config holds N consecutive snapshots) is a candidate
    refinement for the n>=100 review â€” NOT wired now).
    Equivalence note (verified 2026-07-05): 'other NOT in band' == 'other CLV
    < 5%' under the below-other condition â€” the cheaper book always has the
    HIGHER CLV, so the other book can never sit above band while we're in it.
    Backtest difference set is empty; the n=286 numbers apply to this rule
    exactly as written.
    Shared-price only. One bet/fixture (first qualifying pre-KO snapshot, max CLV).r]   Úpin_homeÚpin_drawÚpin_awayNr4   r"   r$   r   r   r   r-   r   r.   rG   rY   r\   rT   rZ   r[   ©ÚclvÚpxÚwonrC   r*   rA   ÚaskÚfairrL   rY   r\   rT   r-   r.   r   rZ   Úwon_displayr[   c                 S   s   | d S ©Nrt   r   ©Údr   r   r   rU   	  r,   zfire_xlow.<locals>.<lambda>rV   )
r   r   r@   ÚSIDXrh   rF   ÚBANDrg   rj   Úmax©Úfxr   ÚphÚpd_ÚpaÚpfÚcandsrC   ÚsiÚkmÚkaÚpmÚpa_rA   ru   Úotherr*   rt   Z	other_clvrv   r   r   r   Ú	fire_xlowÎ   s<    .(@ $,&.ÿó
r�   c                 C   s   dd„ dd„ | D ƒD ƒS )Nc                 S   s   g | ]}|r|‘qS r   r   ©r(   Úbr   r   r   r+     r,   z!classify_xlow.<locals>.<listcomp>c                 s   s   | ]}t |ƒV  qd S r   )r�   ©r(   r�   r   r   r   r/     r,   z classify_xlow.<locals>.<genexpr>r   ©rm   r   r   r   Úclassify_xlow  s    r’   Úmain)ÚGAP_LEAGUESZUSLZPL1c                 C   s–  | d t vrdS | d D �]v}t| d¡ƒt| d¡ƒt| d¡ƒ  }}}d|||fv s|dkr|dkr|dkstqt|||fƒ}g }t ¡ D �]æ\}}t|d|ƒ\}	}
t|d	|ƒ\}}|	du sŽ|du sŽd
|	  k rÜdk rŽn qŽd
|  k rôdk søqŽ qŽ|	|k�r|	|
|df\}}}}n|||	d	f\}}}}|du sŽd
|  k �rLdk �sPqŽ qŽ|| | ||  }|| | ||  }||k rŽtd
 |  k�r td k �s¤qŽ qŽtd
 |  k�rÄtd k �rÊqŽ nqŽ| d dk�râ|dk�pö| d dk�oö|dk}| t	||||||||| | d¡| d |  d¡| d | d¡| d¡| d |  d¡|  d¡|dk�rbdndk|  d¡d�¡ qŽ|rt
|dd„ d�  S qdS )zNfire_xlow with Polymarket-US standing in for Polymarket.com, GAP_LEAGUES only.rT   Nr]   rp   rq   rr   r4   r"   r#   r   r   r   r-   r   r.   rG   rY   r\   rZ   r[   rs   c                 S   s   | d S rz   r   r{   r   r   r   rU   F  r,   z"fire_xlow_polyus.<locals>.<lambda>rV   )r”   r   r   r@   r}   rh   rF   r~   rg   rj   r   r€   r   r   r   Úfire_xlow_polyus'  s>    .(@ $,&.ÿ÷

r•   c                 C   s   dd„ dd„ | D ƒD ƒS )Nc                 S   s   g | ]}|r|‘qS r   r   rŽ   r   r   r   r+   J  r,   z(classify_xlow_polyus.<locals>.<listcomp>c                 s   s   | ]}t |ƒV  qd S r   )r•   r�   r   r   r   r/   J  r,   z'classify_xlow_polyus.<locals>.<genexpr>r   r‘   r   r   r   Úclassify_xlow_polyusI  s    r–   c                 C   s   |   d¡tv rt| ƒS t| ƒS )aß  THE forward-test firing function: xlow with whichever confirming book
    the league actually has.

    Poly.com where it exists; Poly-US in GAP_LEAGUES (USL, PL1), which have
    poly_tag=None and therefore no Poly.com quotes at all. Without this the two
    are permanently invisible to the forward test while being actively traded --
    which is why they were put in the scope (user, 2026-08-23).

    HONEST NOTE. The n=286 backtest confirmed against Poly.com only, so Poly-US
    as a confirming book is UNVALIDATED -- it is a thinner, newer venue. The
    primary is now a mix of two confirmations. Those bets are a small minority
    (USL 50, PL1 21 vs ~190 Poly.com) and the row `xlow_polyus` still reports
    them alone, so the mix can always be decomposed.

    No double counting: the two paths are mutually exclusive by league, because
    fire_xlow_polyus returns None outside GAP_LEAGUES and fire_xlow cannot fire
    inside them (no Poly.com prices to compare against).
    rT   )r   r”   r•   r�   ©r�   r   r   r   Úfire_xlow_primaryM  s    r˜   >   ZP1ZAR1ZN1ZD2ZD1ZSP1ZSP2ZE0ZE2ZNO1ÚBSAZSC0ZT1ZF1ZI2ÚCL1ZE1ZSE1ZMLSZI1z
2026-08-23z
2026-09-01r™   rš   ZNWSLZKO1c                 C   s   |   d¡tv S )NrT   )r   ÚPRIMARY_LEAGUESr—   r   r   r   Úin_primary_leagues¸  s    rœ   c                 C   s   dd„ dd„ | D ƒD ƒS )z„xlow over PRIMARY_LEAGUES -- the primary forward scope. Uses
    fire_xlow_primary, so USL/PL1 are included via the Poly-US confirm.c                 S   s   g | ]}|r|‘qS r   r   rŽ   r   r   r   r+   Å  s   ÿz$classify_xlow_bt.<locals>.<listcomp>c                 s   s   | ]}t |ƒrt|ƒV  qd S r   ©rœ   r˜   r�   r   r   r   r/   Å  s   ÿz#classify_xlow_bt.<locals>.<genexpr>r   r‘   r   r   r   Úclassify_xlow_btÂ  s    rž   c                 C   s   dd„ dd„ | D ƒD ƒS )a  xlow over everything outside the primary scope. Tracking only, never
    pooled into the primary -- see PRIMARY_LEAGUES. Note this now includes the
    two leagues REMOVED from the scope (BSA, CL1), so they stay measured rather
    than disappearing from the page.c                 S   s   g | ]}|r|‘qS r   r   rŽ   r   r   r   r+   Î  s   ÿz'classify_xlow_newlg.<locals>.<listcomp>c                 s   s   | ]}t |ƒst|ƒV  qd S r   r�   r�   r   r   r   r/   Î  s   ÿz&classify_xlow_newlg.<locals>.<genexpr>r   r‘   r   r   r   Úclassify_xlow_newlgÉ  s    rŸ   gš™™™™™¹?c                 C   s   dd„ t | ƒD ƒS )Nc                 S   s   g | ]}|d  t kr|‘qS ©ru   ©ÚXLOW_PRICE_FLOORrŽ   r   r   r   r+   Þ  r,   z%classify_xlow_f10.<locals>.<listcomp>)r’   r‘   r   r   r   Úclassify_xlow_f10Ý  s    r£   z
2026-09-20é   ç      ð¿c                 C   sâ   t tƒt tƒd  }}}g g  }}t| dd„ d�D ]¦}|d |krt| ¡ D ]\}	}
||	  |
¡ qJt tƒ|d  }}||d  r�t||d  ƒnd}t|ƒo²|d |ko²|d |k }|r¼|n| |¡ ||d   |¡ q2||fS )	a«  Point-in-time exclusion. Returns (kept, dropped).

    A bet is dropped when its league's z -- computed over that league's bets
    settled on STRICTLY EARLIER game dates -- is below `floor`, once there are
    at least `min_n` of them. Same-day bets cannot inform each other: they wait
    in `pending` and fold in when the date rolls, because a bet struck at 13:00
    cannot know the result of one kicking off at 15:00.
    Nc                 S   s   | d |   d¡pdfS )NrY   r\   r   ©r   )r�   r   r   r   rU   +  r,   zzgate_walk.<locals>.<lambda>rV   rY   rT   ÚnÚz)r   r^   ri   rh   ÚextendÚstatÚboolrg   )ÚbetsÚmin_nÚfloorZhistÚpendingZcurÚkeptÚdroppedr�   ÚlgÚbsr   Zbadr   r   r   Ú
zgate_walk   s    	
  r´   c           	      C   sz   t tƒ}| D ]}||d   |¡ qi }| ¡ D ]D\}}t|ƒ}|sFq0t||d |ko`|d |k |d |k d�||< q0|S )aS  Which leagues stand in DROP state now, over every settled bet given.

    CURRENT state, for display -- what the gate would do to the next fixture --
    not the walk-forward record. Leagues still under the warmup come back
    flagged `warm` so the panel can show what it is waiting on rather than
    silently treating them as fine.
    rT   r§   r¨   )r±   Zwarm)r   r^   rg   rh   rª   rj   )	r¬   r­   r®   Zbyr�   Úoutr²   r³   r   r   r   r   Úzgate_state6  s    
ÿr¶   c                 C   s,   dd„ | D ƒ}t t|ƒƒ\}}dd„ |D ƒS )zðPrimary scope, z-gate applied, POST-ZGATE_FREEZE bets only.

    The walk runs over the whole OOS history so the gate's state is warm, then
    the result is cut to post-freeze fixtures: state may use the history,
    evidence may not.
    c                 S   s   g | ]}|d  t kr|‘qS ©rY   ©ÚFREEZE_DATEr�   r   r   r   r+   Q  r,   z'classify_xlow_zgate.<locals>.<listcomp>c                 S   s   g | ]}|d  t kr|‘qS r·   ©ÚZGATE_FREEZErŽ   r   r   r   r+   S  r,   )r´   rž   )rm   Úoosr°   r&   r   r   r   Úclassify_xlow_zgateJ  s    r½   c                 C   s    dd„ | D ƒ}dd„ t |ƒD ƒS )zÝThe UNGATED primary over the same post-freeze window -- the control.

    Without it the gated number has nothing to be read against and would get
    compared to the all-history primary, which is a different window.
    c                 S   s   g | ]}|d  t kr|‘qS r·   r¸   r�   r   r   r   r+   [  r,   z'classify_xlow_postz.<locals>.<listcomp>c                 S   s   g | ]}|d  t kr|‘qS r·   rº   rŽ   r   r   r   r+   \  r,   )rž   )rm   r¼   r   r   r   Úclassify_xlow_postzU  s    r¾   )iÄÿÿÿé-   c                 C   s„  d\}}| d D ]B}t | d¡ƒ}|d u s|dkr4qt|t ƒ}||k r|| }}q|d u sd|tkrhd S |}t | d¡ƒt | d¡ƒt | d¡ƒ  }}}d |||fv sÀ|dkrÀ|dkrÀ|dksÄd S t|||fƒ}	g }
t ¡ D �]ˆ\}}t|d	|ƒ\}}t|d
|ƒ\}}|d u sÞ|d u sÞd|  k �r.dk rÞn qÞd|  k �rJdk �sNqÞ qÞ|	| | |	|  }|	| | |	|  }t	d |  k�o’t	d k n  t	d |  k�o²t	d k n   }}||k�rÈqÞ|�rø|�rôd|  k �rìdk �rôn n|n|n|}|d u sÞd|  k �rdk �s qÞ qÞ| d dk�r8|dk�pL| d dk�oL|dk}|
 
tt||ƒ||d�¡ qÞ|
�r€t|
dd„ d�S d S )N)Ng    eÍÍAr]   rH   r   rp   rq   rr   r4   r"   r$   r   r   r-   r   r.   ©rt   ru   rv   c                 S   s   | d S rz   r   r{   r   r   r   rU   ‚  r,   zfire_t60.<locals>.<lambda>rV   )r   r   ÚabsÚ
T60_TARGETÚT60_TOLr@   r}   rh   rF   r~   rg   rj   r   )r�   ZbestZbdr   rM   r|   r‚   rƒ   r„   r…   r†   rC   r‡   rˆ   r‰   rŠ   r‹   ZckZcpZk_inZp_inru   rv   r   r   r   Úfire_t60k  s.    .*F(F4$.rÄ   c                 C   s   dd„ dd„ | D ƒD ƒS )Nc                 S   s   g | ]}|r|‘qS r   r   rŽ   r   r   r   r+   …  r,   z classify_t60.<locals>.<listcomp>c                 s   s   | ]}t |ƒV  qd S r   )rÄ   r�   r   r   r   r/   …  r,   zclassify_t60.<locals>.<genexpr>r   r‘   r   r   r   Úclassify_t60„  s    rÅ   c              	   C   sŒ  g }d}dD �]Z\}}| d D �]F}t | d¡ƒt | d¡ƒt | d¡ƒ  }}}	d|||	fv s|dkr|dkr|	dkszqt|||ƒ\}
}|
du s|du sd	|
  k r®dk rn qd	|  k rÆdk sÊq qd
}t|||	fƒ| }|rð||
 | nd}|du std	 |  k�rtd k �s q q| d dk�r8|dk�pL| d dk�oL|dk}| t|||d�¡  qqq|�sxd|fS t|dd„ d�|fS )z6book='kalshi' / 'poly'. Returns bet dict + have_price.F))r-   r   )r.   r5   r]   rp   rq   rr   Nr4   r   Tr   r   r-   r   r.   rÀ   c                 S   s   | d S rz   r   r{   r   r   r   rU   ˜  r,   zfire.<locals>.<lambda>rV   )r   r   rF   r@   r~   rg   rj   r   )r�   r*   r†   ZhaverC   Zsidxr   r‚   rƒ   r„   rA   ru   r…   rt   rv   r   r   r   Úfireˆ  s    .(@,.rÆ   c                 C   s|   g g  }}| D ]d}t |dƒ\}}t |dƒ\}}|r|s8q|rL|rL| |¡ q|r`|s`| |¡ q|r|s| |¡ q||fS )z>Return (unique_bets, overlap_bets) over shared-price fixtures.r"   r$   )rÆ   rg   )rm   ÚuniqÚoverlapr�   ÚkZkhÚpr‚   r   r   r   Úclassifyš  s    

rË   c                 C   s    t | ƒ}|sd S tdd„ | D ƒƒ}tdd„ | D ƒƒ}tdd„ | D ƒƒ}tdd„ | D ƒƒ}|dkrr|| t |¡ nd}t||| d || d || d |d	�S )
Nc                 s   s   | ]}|d  V  qdS )rv   Nr   rŽ   r   r   r   r/   ¨  r,   zstat.<locals>.<genexpr>c                 s   s.   | ]&}|d  r"d|d  |d  ndV  qdS )rv   r4   ru   r¥   Nr   rŽ   r   r   r   r/   ©  r,   c                 s   s   | ]}|d  V  qdS )ru   Nr   rŽ   r   r   r   r/   ª  r,   c                 s   s"   | ]}|d  d|d    V  qdS )ru   r4   Nr   rŽ   r   r   r   r/   ª  r,   r   g        éd   )r§   ÚwrÚiwrÚroir¨   )r9   r>   ÚmathZsqrtrj   )r¬   r§   ZwinsZnetZmuÚvarr¨   r   r   r   rª   ¥  s    $rª   z
2026-08-09gš™™™™™É?c                 C   s°  | d D �] }t | d¡ƒt | d¡ƒt | d¡ƒ  }}}d|||fv s|dkr|dkr|dksdqt|||fƒ}dD �]0}t|d|ƒ\}}|du sv|du svd	|  k r°dk rvn qvd	|  k rÈdk sÌqv qv|tk rÖqv|t|  }	|	d	krìqv|	| |	 }
td	 |
  k�rtd k rvn qv|| d
 |dk�r0dndk||||	|
| d¡| d |  d¡| d | d
 |  d¡|  d¡|dk�r~dndk|  d¡| d¡| d¡dœ    S qvqdS )aŠ  As fire_xhigh, but returns the FULL context of the qualifying snapshot.

    THE RULE LIVES HERE, ONCE. fire_xhigh() is a thin wrapper over this. The
    dashboard's live xhigh panel needs the side, price and CLV that fired, not
    just the settled bet, and re-implementing the conditions there is exactly
    how a displayed "signal" drifts into disagreeing with the forward test that
    scores it. Anything that wants to know what xhigh does asks this function.

    `won` is meaningless for a fixture with no result yet (ftr is None, so it
    reads False); a caller showing live fixtures must ignore it rather than
    render it as a loss.
    r]   rp   rq   rr   Nr4   ro   r"   r   r   r-   r   r   rG   rY   r\   rT   rZ   r[   r.   )ru   rv   rC   rA   rw   rx   rt   rL   rY   r\   rT   r   rZ   ry   r[   r-   r.   )r   r   r@   rF   ÚXHIGH_PRICE_FLOORr}   r~   )r�   r   r‚   rƒ   r„   r…   rC   rD   rE   rx   rt   r   r   r   Úfire_xhigh_detailÈ  s:    .&
>"
ÿôrÓ   c                 C   s"   t | ƒ}|r|d |d dœS dS )aš  Kalshi mid-CLV in BAND on a side priced at/above the floor.

    No second book required. One bet per fixture: FIRST qualifying pre-KO
    snapshot, settled at the real Kalshi ask -- same non-look-ahead selection
    as fire_xlow, so the two are comparable bet-for-bet.

    Returns only what the scoring path needs, so adding fields to
    fire_xhigh_detail can never quietly change what stat() averages.
    ru   rv   )ru   rv   N)rÓ   )r�   r|   r   r   r   Ú
fire_xhighõ  s    
rÔ   c                 C   s6   g }| D ](}|d t krqt|ƒ}|r| |¡ q|S )zEBets for fixtures AFTER XHIGH_FREEZE only -- see the provenance note.rY   )ÚXHIGH_FREEZErÔ   rg   )rm   rµ   r�   r�   r   r   r   Úclassify_xhigh  s    rÖ   c                    sÒ   t j t¡sdS ttddd��,}t |¡}|jp2g }t|ƒ}W d  ƒ n1 sP0    Y  || krfdS ttdddd��H}tj	|| d�}| 
¡  |D ]‰ | ‡ fdd„| D ƒ¡ q’W d  ƒ n1 sÄ0    Y  dS )	zSMigrate the log header if columns changed (preserve old rows, blank-fill new cols).Nr   rP   rQ   Úw©Ú
fieldnamesc                    s   i | ]}|ˆ   |d ¡“qS )r   r¦   )r(   rÉ   ©Úrowr   r   Ú
<dictcomp>  r,   z&_ensure_log_schema.<locals>.<dictcomp>)r`   ra   ÚexistsÚLOG_CSVrd   re   rf   rÙ   r^   Ú
DictWriterÚwriteheaderÚwriterow)rÙ   rl   ZrdÚoldr   r×   r   rÚ   r   Ú_ensure_log_schema  s    :rã   c                 C   s  | d u rt ƒ } dd„ | D ƒ}|r(t|ƒng }|r8t|ƒng }|rHt|ƒng }dd„ |D ƒ}|rft|ƒng g f\}}|r~t|ƒng }t| ƒ}	|r–t| ƒng }
|r¦t| ƒng }t	 
¡  ¡ tttdœ}d|fd|fd|fd|fd	|	fd
|
fd|fd|fd|fd|ff
D ]”\}}t|ƒ}|�r|d nd||› d�< |�r@t|d dƒnd||› d�< |�rbt|d dƒnd||› d�< |�r„t|d dƒnd||› d�< �q t| ¡ ƒ}t|ƒ tj t¡ }ttdddd��6}tj||d�}|�ræ| ¡  | |¡ W d   ƒ n1 �s0    Y  |S )Nc                 S   s   g | ]}|d  t kr|‘qS r·   r¸   r�   r   r   r   r+     r,   z log_snapshot.<locals>.<listcomp>c                 S   s   g | ]}|d  t kr|‘qS r    r¡   rŽ   r   r   r   r+   %  r,   )Zrun_dateZfreeze_dateZxhigh_freezeZzgate_freezeÚxlow_btÚxlowÚ
xlow_newlgÚxlow_f10ÚxhighÚ
xlow_zgateÚ
xlow_postzÚt60ÚuniquerÈ   r§   r   Z_nrÍ   r4   r   Z_wrrÏ   Z_roir¨   r5   Z_zrE   rP   rQ   rØ   )rn   r’   rž   rŸ   rË   rÅ   rÖ   r½   r¾   r   ZtodayZ	isoformatr¹   rÕ   r»   rª   rK   r^   Úkeysrã   r`   ra   rÝ   rÞ   rd   re   rß   rà   rá   )rm   r¼   rå   rä   ræ   rç   rÇ   rÈ   rë   rè   ré   rê   rÛ   Únamer¬   r   rÙ   Únewrl   r×   r   r   r   Úlog_snapshot  sB    ÿü""&*rð   c                  C   sÖ  t ƒ } tdt› dt| ƒ› d�ƒ ddd„ | D ƒfddd„ | D ƒffD ]ì\}}td	|› d
�ƒ |sjtdƒ qDt|ƒ}dd„ |D ƒ}t|ƒ\}}t|ƒ}d|fd|fd|fd|fd|ffD ]r\}}	t|	ƒ}
|
�rtd|› d|
d d›d|
d d›d|
d d›d|
d d›d|
d d ›�ƒ q¶td|› d!�ƒ q¶tƒ  qDt| ƒ}t|ƒ}td"t	› d#t
› d$�ƒ |�r¤td%|d d›d|d d›d|d d›d|d d›d|d d ›�
ƒ ntd&ƒ td'ƒ td(ƒ t| ƒ td)t› �ƒ d S )*NzFREEZE_DATE=z   total resolved fixtures=Ú
zIN-SAMPLE (<=freeze, reference)c                 S   s   g | ]}|d  t kr|‘qS r·   r¸   ©r(   r   r   r   r   r+   F  r,   zmain.<locals>.<listcomp>zFORWARD (OOS, >freeze)c                 S   s   g | ]}|d  t kr|‘qS r·   r¸   rò   r   r   r   r+   G  r,   z--- z ---z  (no fixtures yet)
c                 S   s   g | ]}|d  t kr|‘qS r    r¡   rŽ   r   r   r   r+   L  r,   zXLOW*  ZXLOWf10zt60    zunique rÈ   z  z  n=r§   Z4dz  WR=rÍ   z4.1fz%  impl=rÎ   z%  ROIturn=rÏ   z+6.1fz%  z=r¨   z+.2fz6  (no shared-price bets -- Poly feed off / off-season)z--- XHIGH (frozen z, Kalshi-only, mid>=z) ---z  XHIGH    n=zA  XHIGH    n=   0  (no fixtures after the freeze yet -- expected)zE  needs ~480 bets to resolve; pre-freeze data is EXCLUDED by design.
zK  * XLOW = PRIMARY rule (cheaper book, below other book AND below Pinnacle)zlogged snapshot -> )rn   Úprintr¹   r9   r’   rË   rÅ   rª   rÖ   rÕ   rÒ   rð   rÞ   )rm   ZlabelÚsubrå   rç   rÇ   rÈ   rë   rî   r¬   r   ZxhZshr   r   r   r“   C  sH    ÿ
*.ÿÿ
(ÿÿ
Ú__main__)T)N)OÚ__doc__re   r_   r`   rÐ   r   r   r   Úcollectionsr   rc   rÞ   r¹   r~   r   r   r   r!   r6   r0   r3   r<   r@   rB   rF   rO   rn   r}   r�   r’   ÚsysÚ_sysÚ_osra   Úinsertrb   ÚdirnameÚabspathÚ__file__Zlive_monitorr”   Ú	Exceptionr•   r–   r˜   Ú	frozensetZBACKTEST_LEAGUESZSCOPE_FROZENZSCOPE_FROZEN_KO1r›   rœ   Zin_backtest_leaguesrž   rŸ   r¢   r£   r»   ZZGATE_MIN_NZZGATE_FLOORr´   r¶   r½   r¾   rÂ   rÃ   rÄ   rÅ   rÆ   rË   rª   rÕ   rÒ   rÓ   rÔ   rÖ   rã   rð   r“   Ú__name__r   r   r   r   Ú<module>   sŽ   %  
/
>
ÿÿ",!ÿ?-

*$
