a
    í»jè8  ã                	   @   sÞ  U d 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 ddl	m
Z
 ddlmZmZ ddlmZ ddlmZ e
d	ƒ d
Ze d¡Ze d¡Zdaee ed< edœdd„Zeeeedœdd„Zeeedœdd„Zd>eee edœdd„Zeeedœdd„ZdZdZ dZ!eed œd!d"„Z"d?eee#e#ee ee# ed#œd$d%„Z$eed&œd'd(„Z%edœd)d*„Z&d+Z'd,Z(d-Z)ee* dœd.d/„Z+ee,d0œd1d2„Z-d@eeee e#ed4œd5d6„Z.edœd7d8„Z/edœd9d:„Z0dAee# ed;œd<d=„Z1dS )Bu�  
Kalshi API client â€” authentication + read-only portfolio methods.

Phase 1 of the auto-trade rollout. Order placement is intentionally NOT
implemented yet â€” that's Phase 2. This module only signs requests and
reads portfolio state.

Auth scheme: RSA-PSS signature of `timestamp + method + path`, base64-encoded,
sent in KALSHI-ACCESS-SIGNATURE header alongside KALSHI-ACCESS-KEY and
KALSHI-ACCESS-TIMESTAMP.
é    N)ÚOptional)ÚPath)Úload_dotenv)ÚhashesÚserialization)Úpadding)ÚRSAPrivateKeyzE:\soccer\.envz-https://api.elections.kalshi.com/trade-api/v2ZKALSHI_ACCESS_KEYZKALSHI_PRIVATE_KEY_PATHÚ_private_key_cache)Úreturnc                  C   s6   t d urt S tstdƒ‚ttƒ ¡ } tj| d d�a t S )Nz'KALSHI_PRIVATE_KEY_PATH not set in .env)Zpassword)r	   ÚPRIVATE_KEY_PATHÚRuntimeErrorr   Ú
read_bytesr   Zload_pem_private_key)Z	pem_bytes© r   úkalshi_trader.pyÚ_load_private_key"   s    r   )ÚmethodÚpathÚts_msr
   c                 C   sV   ||   ¡  |  d¡}tƒ }| |tjt t ¡ ¡tjj	d�t ¡ ¡}t
 |¡ d¡S )z8RSA-PSS sign `timestamp + method + path`, return base64.úutf-8)ZmgfZsalt_length)ÚupperÚencoder   Zsignr   ZPSSZMGF1r   ZSHA256ZDIGEST_LENGTHÚbase64Z	b64encodeÚdecode)r   r   r   ÚmsgÚkeyÚsigr   r   r   Ú_sign-   s    þúr   )r   r   r
   c                 C   sB   t stdƒ‚ttt ¡ d ƒƒ}d| }t| ||ƒ}t ||ddœS )z-Build the three required Kalshi auth headers.z!KALSHI_ACCESS_KEY not set in .enviè  z/trade-api/v2úapplication/json)zKALSHI-ACCESS-KEYzKALSHI-ACCESS-SIGNATUREzKALSHI-ACCESS-TIMESTAMPZAccept)Ú
ACCESS_KEYr   ÚstrÚintÚtimer   )r   r   r   Ú	full_pathr   r   r   r   Ú_auth_headers<   s    ür#   )r   Úparamsr
   c                 C   sP   t d| ƒ}tjt|  ||dd�}|jdkrHtd| › d|j› d|j› �ƒ‚| ¡ S )NZGETé   )Úheadersr$   ÚtimeoutéÈ   zKalshi GET ú -> ú: )r#   ÚrequestsÚgetÚAPI_BASEÚstatus_coder   ÚtextÚjson)r   r$   r&   Úrr   r   r   Ú_getL   s
    

r2   )r   Úbodyr
   c                 C   sX   t d| ƒ}d|d< tjt|  ||dd�}|jdvrPtd| › d|j› d	|j› �ƒ‚| ¡ S )
NZPOSTr   zContent-Typer%   )r&   r0   r'   )r(   éÉ   zKalshi POST r)   r*   )r#   r+   Zpostr-   r.   r   r/   r0   )r   r3   r&   r1   r   r   r   Ú_postT   s    

r5   éc   éd   g       @)Útickerr
   c                 C   s   t d| › �ƒS )uZ   GET /markets/{ticker} â€” public market data (auth not strictly required, but consistent).z	/markets/©r2   )r8   r   r   r   Ú
get_marketj   s    r:   )r8   ÚsideÚcountÚprice_centsÚclient_order_idÚ
subaccountr
   c                 C   s.  |  ¡ }|dkrtdƒ‚|dkr.td|› �ƒ‚t|tƒr@|dk rNtd|› �ƒ‚t|tƒrh|dk sh|tkr|tdt› d|› �ƒ‚|tkr˜td	|› d
t› �ƒ‚|| d }|tkrÄtd|d›dtd›�ƒ‚| d|d›|d d›dddœ}|rî||d< |du�r$t|tƒ�r|dk �rtd|›�ƒ‚||d< td|ƒS )aÞ  
    Place a LIMIT order via the V2 orders endpoint. Hard caps enforced BEFORE
    any HTTP request. V1 (/portfolio/orders) returns 410 as of 2026-07-23 --
    migrated per https://docs.kalshi.com/api-reference/orders/create-order-v2.

    side: 'yes' or 'no' -- kept as the external interface callers already use
    (daily_signals.place_auto_order, etc.), translated internally. V2's `side`
    field is YES-perspective only (bid=buy YES, ask=sell YES) with NO direct
    "buy NO" -- every real caller in this codebase only ever passes 'yes' (a
    separate guard upstream already rejects NO-side auto-trades), so 'no' is
    rejected here rather than silently mapped to the wrong economic action.
    count: integer number of contracts
    price_cents: limit price 1-99
    subaccount: route the order to a Kalshi subaccount. None (default) means
        the primary, which is byte-identical to passing 0.

        WHY THIS EXISTS: Kalshi nets long and short on the same ticker WITHIN a
        subaccount, so xlow buying a leg the maker is short partially closes
        each against the other. Separate subaccounts do not net, which removes
        the conflict instead of trading one bad outcome for another.

        VERIFIED 2026-09-01 against the live API, because the endpoint accepts
        unknown JSON fields SILENTLY -- a 201 proves nothing on its own. Three
        IOC bids priced far below the ask (so they could not fill) were sent
        and then located by order_id: `subaccount: 1` landed in subaccount 1,
        while both no-field and `subaccount_number: 1` landed in the primary.
        So the key is `subaccount`, an int, and a misspelling fails OPEN into
        the primary rather than erroring. Do not rename it casually.
    Znozyside='no' not supported by this client -- V2 has no direct buy-NO order; NO-side auto-trades are rejected upstream anywayZyeszside must be 'yes', got é   zcount must be int >= 1, got zprice_cents must be int 1..z, got zcount z! exceeds MAX_CONTRACTS_PER_ORDER=ç      Y@zdollar exposure $ú.2fz exceeds cap $Zbidz.4fZimmediate_or_cancelZtaker_at_cross)r8   r;   r<   ZpriceZtime_in_forceZself_trade_prevention_typer>   Nr   z+subaccount must be a non-negative int, got r?   z/portfolio/events/orders)ÚlowerÚ
ValueErrorÚ
isinstancer    ÚMAX_PRICE_CENTSÚMAX_CONTRACTS_PER_ORDERÚMAX_DOLLARS_PER_ORDERr5   )r8   r;   r<   r=   r>   r?   Zdollar_exposurer3   r   r   r   Úplace_limit_ordero   s8     
÷
rI   )Úorder_idr
   c                 C   sX   d| › �}t jt| td|ƒdd�}|jdvrFtd|j› d|j› �ƒ‚|jrT| ¡ S i S )aé  Cancel via the V2 endpoint.

    This used the V1 path (/portfolio/orders/{id}) until 2026-08-15, which has
    returned 410 deprecated_v1_order_endpoint since the 2026-07-23 migration --
    placement was moved to V2 that day and cancellation was missed. Nothing
    caught it for three weeks because every order this project sent was
    immediate-or-cancel and so never rested long enough to be cancelled. The
    first resting order (the live maker) could not be cancelled at all.
    z/portfolio/events/orders/ZDELETEr%   )r&   r'   )r(   éÌ   zKalshi DELETE order -> r*   )r+   Údeleter-   r#   r.   r   r/   r0   )rJ   r   r1   r   r   r   Úcancel_order³   s    

ÿ
rM   c                   C   s   t dƒS )u>   GET /portfolio/balance â€” returns {'balance': cents_int, ...}ú/portfolio/balancer9   r   r   r   r   Úget_balanceÉ   s    rO   g      I@)r   r@   )zE:\soccer\STOP_MAKERzE:\soccer\data\AUTO_TRADE_KILLc               	   C   sr   d} t D ]d}zHtdd|id�p i }| t| dd¡p4dƒt| dd¡pFdƒ d 7 } W q tyj   Y  d	S 0 q| S )
zÙCash + open positions across every Kalshi subaccount, in dollars.

    None if ANY subaccount is unreadable: a partial sum would read low and
    halt everything on a network blip, or read high and hide a breach.
    g        rN   r?   ©r$   Zbalancer   Zportfolio_valuerA   N)ÚKALSHI_SUBACCOUNTSr2   Úfloatr,   Ú	Exception)ZtotÚsubÚbr   r   r   Úkalshi_account_valueÚ   s    ÿÿ
rV   )Úwhor
   c              	   C   s¸   t ƒ }|du r"td| › d�ƒ dS |tkr.dS d|d›dtd›d	| › d
�}tD ]T}z<t|ddd��}| |¡ W d  ƒ n1 s‚0    Y  W qN ty    Y qN0 qNtd| ¡  ƒ dS )aŠ  True if account value is below KALSHI_FLOOR_USD -- and if so, engage the
    kill files so Kalshi trading stays stopped until a person clears them.
    Durable on purpose: a stop that a scheduled job silently undoes is not a
    stop.

    Unreadable value -> True for the CALLER (do not place this order) but no
    kill files: a network blip should skip one order, not halt the strategy.
    Nz)[KALSHI FLOOR] account value unreadable (z) -- skipping this orderTFzKalshi floor: account value $rB   z < $z
 (seen by zK) -- all Kalshi trading halted. Delete the kill file(s) by hand to resume.
Úwr   )Úencodingz[KALSHI FLOOR] )rV   ÚprintÚKALSHI_FLOOR_USDÚKALSHI_FLOOR_KILLSÚopenÚwriterS   Ústrip)rW   Úvr   ÚpZfhr   r   r   Úkalshi_floor_breachedë   s"    	ÿ,rb   é2   )r   r   r$   Ú	max_pagesr
   c                 C   sì   g t ƒ dddf\}}}}}||k rØt|p*i ƒ}	|	 dd¡ |rF||	d< t| |	d�pTi }
|
 |¡pbg }|D ]P}| d¡pˆ| d¡pˆ| d	¡}|dur®||v r¤|d
7 }qh| |¡ | |¡ qh|
 d¡}|d
7 }|rØ|sqØq||ddd|d|iS )a  GET every page of a cursor-paginated portfolio endpoint.

    WHY THIS EXISTS. These endpoints return {'<key>': [...], 'cursor': '...'}
    and a single call yields ONE page. get_open_orders used to make that single
    call, so with 111 resting orders it reported 55 -- the live maker's
    duplicate detection and its own "too many resting" abort both read that
    halved number and neither could fire. Silent truncation on a portfolio
    read is worse than an error: every count downstream is quietly wrong.
    Nr   Úlimitr(   ÚcursorrP   rJ   Zfill_idr8   r@   ÚpagesZdeduped)ÚsetÚdictÚ
setdefaultr2   r,   ÚaddÚappend)r   r   r$   rd   ÚoutÚseenrf   rg   Údupra   ÚdZbatchÚitemÚkr   r   r   Ú_get_all  s*    

rs   c                   C   s
   t ddƒS )uC   GET /portfolio/positions â€” all open market positions, every page.z/portfolio/positionsZmarket_positions©rs   r   r   r   r   Úget_positions3  s    ru   c                   C   s   t ddddiƒS )uD   GET /portfolio/orders â€” every resting order, following the cursor.z/portfolio/ordersZordersZstatusZrestingrt   r   r   r   r   Úget_open_orders8  s    rv   )Úafter_tsr
   c                 C   s    i }| dur| |d< t dd|ƒS )u1   GET /portfolio/fills â€” order fills, every page.NZmin_tsz/portfolio/fillsZfillsrt   )rw   r$   r   r   r   Ú	get_fills=  s    rx   )N)NN)Nrc   )N)2Ú__doc__Úosr!   r   Útypingr   r+   Úpathlibr   Zdotenvr   Zcryptography.hazmat.primitivesr   r   Z)cryptography.hazmat.primitives.asymmetricr   Z-cryptography.hazmat.primitives.asymmetric.rsar   r-   Úgetenvr   r   r	   Ú__annotations__r   r   r   ri   r#   r2   r5   rF   rG   rH   r:   r    rI   rM   rO   r[   rQ   r\   rR   rV   Úboolrb   rs   ru   rv   rx   r   r   r   r   Ú<module>   sZ   

  þ
þD  ÿÿ,