a
    "%žjïH  ã                   @   s@  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m	Z	 ddl
Z
ddlmZ ej dd¡ edƒ ddlZe	dƒZdZe d	¡Zd
ZdZdZg d¢Zdd„ Zdd„ Zdd„ Zdd„ Zdd„ Ze	dƒZe	dƒZdZ g d¢Z!dd„ Z"dd„ Z#d d!„ Z$d0d"d#„Z%d$d%„ Z&d&d'„ Z'd(d)„ Z(d1d+d,„Z)d-d.„ Z*e+d/k�r<e*ƒ  dS )2aŸ  Capture REAL-TIME Pinnacle odds alongside our existing API-Football feed.

WHY. Measured 2026-07-28 on 147 fixtures (median 91 pre-match snapshots each):
API-Football's "Pinnacle" money line changes about once every 5.8 HOURS, while
Kalshi's own mid changes every ~2.3h. The sharpest book in the world repricing
half as often as a retail prediction market is not a fact about markets -- it is
a relay artifact. Every number this project rests on (the [5,12) CLV band, the
n=286 backtest, the 144-bet forward test) was computed against that stale anchor,
so an unknown share of what we call "CLV" may be pure time-lag rather than
mispricing.

This logs a fresh Pinnacle line next to the stale one so the question can be
settled with data instead of argument. It TRADES NOTHING and changes no existing
behaviour -- the live pipeline keeps using API-Football until the comparison says
otherwise.

COST CONTROL. pinnapi's free tier is 100 requests/day and returns EVERY soccer
prematch fixture in one call, so one poll covers the whole slate. At
POLL_MINUTES=20 that is 72 calls/day, comfortably inside the cap with headroom
for reruns. Do NOT lower it below ~15 without a paid plan; the API answers 429
with a Retry-After once the cap is hit.

SETUP. Add your key to E:\soccer\.env as:
    PINNAPI_KEY=xxxxxxxx
Type it into the file directly -- do not paste keys into a chat window.

Usage:  python main\pin_fresh_capture.py [--once] [--dry]
é    N)ÚPath)Úload_dotenvzE:\soccer\mainzE:\soccer\.envz(E:\soccer\research\pin_fresh_capture.csvz,https://pinnapi.com/kit/v1/prematch/fixturesZPINNAPI_KEYé   é   éZ   ©%ÚtsÚlcÚhomeÚawayÚ	game_dateZkickoff_utcZpin_event_idZ
pin_leagueZpin_home_nameZpin_away_nameZfresh_hZfresh_dZfresh_aZstale_hZstale_dZstale_aÚfidZmatch_scoreZ
k_home_midZ
k_away_midZ
p_home_midZ
p_away_midZfair_fresh_hZfair_fresh_aZfair_stale_hZfair_stale_aZclv_fresh_k_hZclv_fresh_k_aZclv_fresh_p_hZclv_fresh_p_aZclv_stale_k_hZclv_stale_k_aZclv_stale_p_hZclv_stale_p_aZ
xlow_freshZ
xlow_staleÚverdictc              
   C   sx   z&dt | ƒ dt |ƒ dt |ƒ g}W n tttfy@   Y dS 0 t|ƒ}|dkrVdS |d | |d | |d | fS )zÈProportional devig -> (fair_home, fair_draw, fair_away). Mirrors the
    live pipeline's method exactly so any difference in the comparison comes
    from the LINE, never from the maths applied to it.g      ð?)NNNr   r   é   )ÚfloatÚ	TypeErrorÚ
ValueErrorÚZeroDivisionErrorÚsum)ÚhÚdÚaÚimpÚs© r   úmain/pin_fresh_capture.pyÚdevigC   s    &r   c                 C   s(   | d u s|d u s| dkrd S | | |  S ©Nr   r   )ÚfairZmidr   r   r   Ú_clvQ   s    r   c                    sà   t jt j ‰‰ ‡ ‡fdd„}d| ||fd|||ffD ]¦\}}}	}
|	r4|
r4d|	  k r\dk r4n q4d|
  k rtdk sxq4 q4t||	ƒt||
ƒ }}|	|
k r´||ƒr´||ƒs´|› d�  S |
|	k r4||ƒr4||ƒs4|› d�  S q4d	S )
aH  Which side/book the frozen xlow rule fires on, or '' for no signal.

    Reimplemented here rather than imported because live_monitor's version
    reads a snapshot row keyed by column name; the RULE is identical -- bet the
    cheaper book when its CLV is in [5%,12%) and the other book is NOT in band,
    home/away only.
    c                    s    | d uoˆ|   koˆ k S   S ©Nr   )Úc©ÚhiÚlor   r   Ú<lambda>`   ó    zxlow_verdict.<locals>.<lambda>r
   r   r   r   z/kalshiz/polyÚ )ÚLMZEDGE_THRESH_HOMEZEDGE_UPPER_CAP_HOMEr   )Zfair_hZfair_aÚkhÚkaÚphÚpaZinbandZsider   Zk_midZp_midZckZcpr   r"   r   Úxlow_verdictW   s    

ÿ6r-   c                 C   s   d  dd„ | pd ¡ D ƒ¡S )Nr'   c                 s   s   | ]}|  ¡ r|V  qd S r    ©Úisalnum©Ú.0Zchr   r   r   Ú	<genexpr>n   r&   znorm.<locals>.<genexpr>)ÚjoinÚlower©r   r   r   r   Únormm   s    r6   c                 C   s*   dd„ d  dd„ | pd ¡ D ƒ¡ ¡ D ƒS )Nc                 S   s   h | ]}t |ƒd kr|’qS )r   )Úlen)r1   Útr   r   r   Ú	<setcomp>r   s   ÿztokens.<locals>.<setcomp>r'   c                 s   s   | ]}|  ¡ r|nd V  qdS )ú Nr.   r0   r   r   r   r2   r   s   ztokens.<locals>.<genexpr>)r3   r4   Úsplitr5   r   r   r   Útokensq   s    
ÿr<   z$E:\soccer\research\pin_board_log.csvz!E:\soccer\data\pinnapi_state.jsonéU   ©
r   Úevent_idÚ	league_idÚleague_nameÚstartsr
   r   r   r   r   c                   C   s0   zt  tjdd�¡W S  ty*   i  Y S 0 d S )Núutf-8©Úencoding)ÚjsonÚloadsÚSTATEÚ	read_textÚ	Exceptionr   r   r   r   Ú_stateŒ   s    rK   c                 C   sD   z,t jjddd� t jtj| dd�dd� W n ty>   Y n0 d S )NT)ÚparentsÚexist_okr   )ÚindentrC   rD   )rH   ÚparentÚmkdirZ
write_textrF   ÚdumpsÚOSErrorr5   r   r   r   Ú_save_state“   s
    rS   c                 C   sB   t j t jj¡ d¡}|  d¡|kr6|d | d< | d< | d tk S )zDTrue if we may spend a call today. Self-enforced -- see QUOTA above.z%Y-%m-%dÚdayr   Úcalls)ÚdtÚdatetimeÚnowÚtimezoneÚutcÚstrftimeÚgetÚDAILY_BUDGET)ÚstZtodayr   r   r   Ú
_budget_ok›   s    r_   c                 C   s~   t stdƒ‚dti}| r | |d< tjt|dt idd�}|jdkrXtd|jd	d
… › �ƒ‚| ¡  | 	¡ }| dg ¡| d¡fS )zA(events, last_cursor). With `since`, returns only CHANGED events.zPINNAPI_KEY not set in .envZsport_idÚsincezx-portal-apikeyé-   )ÚparamsZheadersZtimeouti­  zrate limited: Néx   ÚeventsÚlast)
ÚKEYÚRuntimeErrorÚSPORT_SOCCERÚrequestsr\   ÚBASEZstatus_codeÚtextZraise_for_statusrF   )r`   rb   ÚrÚjr   r   r   Úfetch_fresh£   s    ÿ
rn   c                    sœ  |  di ¡}t ¡  }d}ttdddd���}tj|td�}|rH| ¡  | D ]ì}| d¡p\i  d	¡pfi  d
¡ppi ‰t	‡fdd„dD ƒƒsŠqLt
| d¡ƒ}	ˆ d¡› dˆ d¡› dˆ d¡› �}
| |	¡|
krÎqL|
||	< | ˆ ¡ | d¡| d¡| d¡| d¡| d¡| d¡ˆ d¡ˆ d¡ˆ d¡dœ
¡ |d7 }qLW d  ƒ n1 �sP0    Y  ˆtjdd� ‰ ‡ ‡fdd„| D ƒ‰‡fdd„| ¡ D ƒ|d< |S )a½  Append events whose money line MOVED since we last saw them.

    Deduping here rather than logging every snapshot is what keeps a 20-minute
    full-board capture affordable: ~2,400 events x 72 polls would be ~170k rows
    a day of mostly identical numbers, where the movements are a small fraction
    of that. The line we did NOT log is recoverable -- it is whatever the last
    logged row for that event_id says. Returns rows written.
    Úseenr   r   r'   rC   ©ÚnewlinerE   ©Z
fieldnamesÚperiodsÚnum_0Ú
money_linec                 3   s   | ]}ˆ   |¡V  qd S r    ©r\   ©r1   Úk©Úmlr   r   r2   Å   r&   zlog_board.<locals>.<genexpr>©r
   Údrawr   r?   r
   ú/r|   r   r@   rA   rB   r>   r   Né   ©Zhoursc                    s0   h | ](}t | d ¡ƒpˆˆ krt| d¡ƒ’qS )rB   r?   )Ú_ptr\   Ústr)r1   Úe)ÚcutoffrX   r   r   r9   Õ   s   ÿzlog_board.<locals>.<setcomp>c                    s   i | ]\}}|ˆ v r||“qS r   r   )r1   rx   Úv)Úlive_idsr   r   Ú
<dictcomp>×   r&   zlog_board.<locals>.<dictcomp>)Ú
setdefaultÚBOARDÚexistsÚopenÚcsvÚ
DictWriterÚBOARD_FIELDSÚwriteheaderr\   Úallr�   ÚwriterowÚ	isoformatrV   Ú	timedeltaÚitems)rd   rX   r^   ro   ÚnewÚnÚfhÚwr‚   ZeidZsigr   )rƒ   r…   rz   rX   r   Ú	log_board³   s8    	
"&û*r˜   c              	   C   sP   zt j t| ƒ dd¡¡}W n ttfy4   Y d S 0 |jr@|S |jt jj	d�S )NÚZz+00:00©Útzinfo)
rV   rW   Úfromisoformatr�   Úreplacer   r   r›   rY   rZ   )r   r   r   r   r   r€   Û   s
    r€   c                    sÐ  g }|D ]€}|  d¡pi   d¡p"i   d¡p,i ‰ t‡ fdd„dD ƒƒsFqt|  d¡ƒ}|du r^q| ||ˆ t|  d	¡ƒt|  d
¡ƒB f¡ qg }| D �]6}|  d¡}t|tƒr¸tj 	|¡}zTt|d ƒ 
d¡dd… \}}	tj|j|j|jt|ƒt|	ƒt tjdd�¡d�}
W n t�y$   Y q’Y n0 t|  d	¡ƒt|  d
¡ƒB }d\}}|D ]R\}}‰ }t||
  ¡ ƒtd k�rx�qNt||@ ƒ}||k�rN||ˆ f| }}�qN|r’|dkr’| ||d |d ||
f¡ q’|S )a×  Pair our watchlist fixtures to Pinnacle events.

    Team names differ between providers (API-Football vs Pinnacle), so match on
    kickoff proximity FIRST -- that is the strong, provider-independent signal --
    then require some token overlap on the names. A pairing that agrees on time
    but shares no team token is rejected rather than guessed at; a wrong pairing
    would silently compare two different matches' odds, which is worse than a
    missing row.
    rs   rt   ru   c                 3   s   | ]}ˆ   |¡V  qd S r    rv   rw   ry   r   r   r2   ð   r&   zmatch_events.<locals>.<genexpr>r{   rB   Nr
   r   r   Zkickoffú:r   éüÿÿÿr   rš   r   é<   r   r   )r\   r�   r€   Úappendr<   Ú
isinstancer�   rV   Údaterœ   r;   rW   ZyearZmonthrT   ÚintrY   r’   rJ   ÚabsZtotal_secondsÚKO_TOLERANCE_MINr7   )Úmatchesrd   Úidxr‚   r^   ÚoutÚmÚgdZhhÚmiÚkoZoursZbestZbscoreZtoksZscorer   ry   r   Úmatch_eventsã   s@    
",


ÿ


r®   Fc           !   *      s¢  t  ¡ \}}tƒ }t|ƒs:td| d¡› dt› d�ƒ dS tƒ \}}tj	 
tjj¡}| dd¡d |d< |rt||d< | r|dn
t|||ƒ}| s”t|ƒ t||ƒ‰ t|d›d	t|ƒ› d
|› d|d › dt› dt|ƒ› dtˆ ƒ› d�ƒ | �rÂˆ D ]n\}}	}
}}td|d d›d|d d d… d›d|	d d d… d›d|› d|
d › d|
d › d|
d › �ƒ qì‡ fdd„|D ƒ}|d d… D ]>}td|d d›d|d d d… › d |d d d… › �ƒ �qztˆ ƒS t ¡  }ttd!d"d#d$���ª}tj|td%�}|�rü| ¡  ˆ D �]v\}}	}
}}i }| d&¡�r^z$t j|d& d'd(�\}}|�p@i }W n t�y\   i }Y n0 | d)¡}t|tƒ�ržztj |¡}W n t�yœ   d }Y n0 i  }}z&t  |d |d |d |¡�pÈi }W n t�yä   i }Y n0 z&t   |d |d |d |¡�pi }W n t�y$   i }Y n0 | d*¡| d+¡ }}| d,¡| d-¡ }}t!|
 d¡|
 d¡|
 d¡ƒ\}}}t!| d.¡| d/¡| d!¡ƒ\}}}t"||||||ƒ}t"||||||ƒ}||k�rÎd0} n(|�rà|�sàd1} n|�rò|�sòd2} nd3} zt| d&¡�rh|d4k�rh|
 d¡�rh|
 d¡�rh|
 d¡�rht  #d5|d& › �t$|
d ƒt$|
d ƒt$|
d ƒd6œ¡ W n t�y~   Y n0 | %| &¡ | d¡| d¡| d¡t| d)¡ƒ| 'tjj¡ &¡ |	 d7¡|	 d8¡|	 d¡|	 d¡|
 d¡|
 d¡|
 d¡| d.¡| d/¡| d!¡| d&¡|||||||||t(||ƒt(||ƒt(||ƒt(||ƒt(||ƒt(||ƒt(||ƒt(||ƒ||| d9œ%¡ �q W d   ƒ n1 �s�0    Y  tˆ ƒS ):Nz   [pinnapi] daily budget spent (rU   r}   z) -- skipping this pollr   r   re   ú%H:%M:%Sz  z	 events (z moved, call z), z watchlist fixtures -> z matchedz   r	   Z5sr:   r
   é   Z18sz <-> z (score z)  r|   r   c                    s"   g | ]}|d d„ ˆ D ƒvr|‘qS )c                 S   s   g | ]}|d  ‘qS )r   r   )r1   Úpr   r   r   Ú
<listcomp>0  r&   z$cycle.<locals>.<listcomp>.<listcomp>r   )r1   rª   ©Zpairsr   r   r²   0  r&   zcycle.<locals>.<listcomp>é
   z   UNMATCHED r   z v r   r'   rC   rp   rr   r   i€Q )Zmax_ager   Zkalshi_home_midZkalshi_away_midZpoly_home_midZpoly_away_midr   r   ZsameZ
fresh_onlyZ
stale_onlyZdifferr   zpinodds:)r   r   r   r?   rA   r   ))r(   Zload_watchlist_filerK   r_   Úprintr\   r]   rn   rV   rW   rX   rY   rZ   r˜   rS   r®   r7   ÚOUTr‰   rŠ   r‹   rŒ   ÚFIELDSrŽ   Zfetch_prematch_oddsrJ   r¢   r�   r£   rœ   r   Zfetch_kalshiZ
fetch_polyr   r-   Z_shared_putr   r�   r‘   Z
astimezoner   )!ÚdryÚ_r§   r^   rd   re   rX   Zwroterª   r‚   rz   Zscr­   Z	unmatchedr”   r–   r—   ZstaleZpinZgd_ZkbZpbr)   r*   r+   r,   ZffhZffaZfshZfsaZx_freshZx_staler   r   r³   r   Úcycle  sæ    

ÿÿÿþþ
8ÿÿÿÿ
<



&
&
$$
:
ÿÿÿ
ë,rº   c               
   C   s€   dt jv } dt jv }zt|d� W n> ty` } z&ttj ¡ d›d|› �ƒ W Y d }~n
d }~0 0 | s||rlq|t 	t
d ¡ qd S )Nz--oncez--dry)r¸   r¯   z	  error: r    )ÚsysÚargvrº   rJ   rµ   rV   rW   rX   ÚtimeÚsleepÚPOLL_MINUTES)Úoncer¸   r‚   r   r   r   Úmain¡  s    

0rÁ   Ú__main__)N)F),Ú__doc__r‹   rW   rV   rF   Úosr»   r½   Úpathlibr   ri   Zdotenvr   ÚpathÚinsertZlive_monitorr(   r¶   rj   Úgetenvrf   r¿   rh   r¦   r·   r   r   r-   r6   r<   rˆ   rH   r]   r�   rK   rS   r_   rn   r˜   r€   r®   rº   rÁ   Ú__name__r   r   r   r   Ú<module>   sP   

(,
 
