You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
248 lines
8.4 KiB
248 lines
8.4 KiB
|
3 weeks ago
|
import aiohttp
|
||
|
|
from decimal import Decimal as D, ROUND_CEILING
|
||
|
|
import dill as pickle
|
||
|
|
import time
|
||
|
|
import os
|
||
|
|
import websockets
|
||
|
|
import requests
|
||
|
|
import urllib
|
||
|
|
import json
|
||
|
|
import aiostream
|
||
|
|
import asyncio
|
||
|
|
import pprint
|
||
|
|
import yahoo_stream
|
||
|
|
from utils import *
|
||
|
|
from piecewise import *
|
||
|
|
|
||
|
|
KALSHI_ROOT = "https://trading-api.kalshi.com/v1" if not DEMO else "https://demo-api.kalshi.co/v1"
|
||
|
|
KALSHI_ORIGIN = "https://kalshi.com" if not DEMO else "https://demo.kalshi.co"
|
||
|
|
KALSHI_AUTH = read_auth("kalshi") if not DEMO else read_auth("kalshi_demo")
|
||
|
|
KALSHI_SESSION_FILE = ".kalshi_session" if not DEMO else ".kalshi_demo_session"
|
||
|
|
|
||
|
|
COMMON_HEADERS = {
|
||
|
|
"accept": "application/json",
|
||
|
|
"accept-encoding": "gzip, deflate, br",
|
||
|
|
"accept-language": "en-US,en;q=0.9",
|
||
|
|
"origin": KALSHI_ORIGIN,
|
||
|
|
"referer": "https://kalshi.com/" if not DEMO else "https://demo.kalshi.co/",
|
||
|
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36",
|
||
|
|
}
|
||
|
|
|
||
|
|
#TODO: figure out what prices actually applies here
|
||
|
|
def convert_contract_hundred_lower(data):
|
||
|
|
rem = 0
|
||
|
|
ret = []
|
||
|
|
for p, s in data:
|
||
|
|
s += rem
|
||
|
|
if s//100: ret.append((p*100, s//100))
|
||
|
|
rem = s%100
|
||
|
|
return ret
|
||
|
|
|
||
|
|
#TODO: unittests for this
|
||
|
|
def convert_contract_avg(data):
|
||
|
|
rem = 0
|
||
|
|
remp = 0
|
||
|
|
ret = []
|
||
|
|
for p, s in data:
|
||
|
|
if rem:
|
||
|
|
u = min(100-rem, s)
|
||
|
|
remp += u*p
|
||
|
|
rem += u
|
||
|
|
s -= u
|
||
|
|
|
||
|
|
if rem==100:
|
||
|
|
ret.append((remp, 1))
|
||
|
|
rem=0
|
||
|
|
remp=0
|
||
|
|
if s:
|
||
|
|
if s//100: ret.append((p*100, s//100))
|
||
|
|
if s%100:
|
||
|
|
rem = s%100
|
||
|
|
remp = rem * p
|
||
|
|
return ret
|
||
|
|
|
||
|
|
def convert_sp_rulebook(data):
|
||
|
|
if data['Above/Below/Between']=='above':
|
||
|
|
return (cents(data["Value"])+1, None)
|
||
|
|
if data['Above/Below/Between']=='below':
|
||
|
|
return (None, cents(data["Value"])-1)
|
||
|
|
return tuple(map(cents, data["Value"].split('-')))
|
||
|
|
|
||
|
|
def convert_nasdaq_rulebook(data):
|
||
|
|
if data['Above/Below/Between']=='above':
|
||
|
|
return (cents(data["Value"])+1, None)
|
||
|
|
if data['Above/Below/Between']=='below':
|
||
|
|
return (None, cents(data["Value"])-1)
|
||
|
|
return tuple(map(cents, data["Value"].split(' and ')))
|
||
|
|
|
||
|
|
def parse_orderbook_message(message):
|
||
|
|
msg = message["msg"]
|
||
|
|
mid = msg["market_id"]
|
||
|
|
bids = [(int(p), s) for p, s in msg.get("yes", [])]
|
||
|
|
asks = [(100 - int(p), s) for p, s in msg.get("no", [])]
|
||
|
|
asks.sort()
|
||
|
|
bids.sort()
|
||
|
|
bids.reverse()
|
||
|
|
|
||
|
|
return mid, bids, asks
|
||
|
|
|
||
|
|
def calc_fee(p, cs, is_index):
|
||
|
|
up = D(p) / D(cs)
|
||
|
|
return int((
|
||
|
|
up * (D(100) - up) *
|
||
|
|
D('0.035' if is_index else '0.07')
|
||
|
|
* D(cs) / D(100)
|
||
|
|
).to_integral_value(rounding=ROUND_CEILING))
|
||
|
|
|
||
|
|
def apply_btic_shift(lims, btic, bid):
|
||
|
|
leeway = 100 if bid else -100
|
||
|
|
leeway = 0
|
||
|
|
return (
|
||
|
|
None if lims[0] is None else lims[0]+btic-leeway,
|
||
|
|
None if lims[1] is None else lims[1]+btic+leeway,
|
||
|
|
)
|
||
|
|
|
||
|
|
def conv_ask_to_pc(lims, p, s, trans_hundred, is_index):
|
||
|
|
fee = calc_fee(p, 100 if trans_hundred else 1, is_index)
|
||
|
|
return (conv_lim_pc(*lims) + (-p -fee), s, p)
|
||
|
|
|
||
|
|
def conv_bid_to_pc(lims, p, s, trans_hundred, is_index):
|
||
|
|
fee = calc_fee(p, 100 if trans_hundred else 1, is_index)
|
||
|
|
return (-conv_lim_pc(*lims) + (p - fee), s, p)
|
||
|
|
|
||
|
|
class KalshiSession:
|
||
|
|
def __init__(self, s):
|
||
|
|
self.s = s
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
return "kalshi"
|
||
|
|
|
||
|
|
async def login(self):
|
||
|
|
self.s.headers.clear()
|
||
|
|
self.s.headers.update(COMMON_HEADERS)
|
||
|
|
self.s.cookie_jar.clear()
|
||
|
|
|
||
|
|
if os.path.exists(KALSHI_SESSION_FILE):
|
||
|
|
with open(KALSHI_SESSION_FILE, 'rb') as f:
|
||
|
|
self.user_id, cookies, csrf_token, self.frontend_session = pickle.load(f)
|
||
|
|
self.s.headers['x-csrf-token'] = csrf_token
|
||
|
|
self.s.cookie_jar._cookies = cookies
|
||
|
|
else:
|
||
|
|
print("doing new login")
|
||
|
|
async with self.s.post(
|
||
|
|
KALSHI_ROOT+"/log_in",
|
||
|
|
json = KALSHI_AUTH,
|
||
|
|
) as resp:
|
||
|
|
self.user_id = (await resp.json())["user_id"]
|
||
|
|
self.s.headers['x-csrf-token'] = resp.headers['x-csrf-token']
|
||
|
|
self.frontend_session = await self.get_frontend_session()
|
||
|
|
|
||
|
|
with open(KALSHI_SESSION_FILE, 'wb') as f:
|
||
|
|
pickle.dump((self.user_id, self.s.cookie_jar._cookies, self.s.headers['x-csrf-token'], self.frontend_session), f)
|
||
|
|
|
||
|
|
async def get_frontend_session(self):
|
||
|
|
async with self.s.post(
|
||
|
|
f"{KALSHI_ROOT}/users/{self.user_id}/frontend_sessions",
|
||
|
|
json = {"cancelToken":{"promise":{}}},
|
||
|
|
) as resp:
|
||
|
|
return (await resp.json())['session_id']
|
||
|
|
|
||
|
|
def open_websocket(self):
|
||
|
|
ws_url = (
|
||
|
|
"wss://" + KALSHI_ROOT.split("://")[1] + "/ws?csrfToken=" +
|
||
|
|
urllib.parse.quote_plus(self.s.headers['x-csrf-token']) +
|
||
|
|
"&frontend_session=" +
|
||
|
|
urllib.parse.quote_plus(self.frontend_session)
|
||
|
|
)
|
||
|
|
return websockets.connect(
|
||
|
|
ws_url,
|
||
|
|
extra_headers = {
|
||
|
|
**self.s.headers,
|
||
|
|
"Cookie": self.s.cookie_jar.filter_cookies(ws_url).output(header=''),
|
||
|
|
},
|
||
|
|
origin = KALSHI_ORIGIN,
|
||
|
|
)
|
||
|
|
|
||
|
|
async def markets_from_ticker(self, ticker, converter):
|
||
|
|
lims = {}
|
||
|
|
async with self.s.get(f"{KALSHI_ROOT}/events/{ticker}") as resp:
|
||
|
|
for m in (await resp.json())['event']['markets']:
|
||
|
|
lims[m["id"]] = converter(m['rulebook_variables'])
|
||
|
|
return lims
|
||
|
|
|
||
|
|
async def execute_order(self, mid, p, s, buy):
|
||
|
|
async with self.s.post(
|
||
|
|
f"{KALSHI_ROOT}/users/{self.user_id}/orders",
|
||
|
|
json = {
|
||
|
|
"count": s*100,
|
||
|
|
"price": p//100 if buy else 100-p//100,
|
||
|
|
"max_cost_cents": 0,
|
||
|
|
"sell_position_capped": False,
|
||
|
|
"expiration_unix_ts": int(time.time())+10,
|
||
|
|
"market_id": mid,
|
||
|
|
"side": "yes" if buy else "no"
|
||
|
|
},
|
||
|
|
) as resp:
|
||
|
|
pprint.pprint("kalshi order")
|
||
|
|
pprint.pprint(resp)
|
||
|
|
pprint.pprint(await resp.json())
|
||
|
|
pprint.pprint("")
|
||
|
|
|
||
|
|
async def orderbook_stream(self, ticker, converter, btic_ticker, is_index=True, trans_hundred=True):
|
||
|
|
lims = await self.markets_from_ticker(ticker, converter)
|
||
|
|
async with self.open_websocket() as ws:
|
||
|
|
for i, mid in enumerate(lims):
|
||
|
|
await ws.send(json.dumps({
|
||
|
|
"cmd": "subscribe",
|
||
|
|
"id": i+1,
|
||
|
|
"params": {
|
||
|
|
"channels": ["orderbook"],
|
||
|
|
"market_id": mid,
|
||
|
|
},
|
||
|
|
}))
|
||
|
|
|
||
|
|
book = {}
|
||
|
|
async with aiostream.stream.ziplatest(
|
||
|
|
ws,
|
||
|
|
yahoo_stream.stream_ticker(btic_ticker),
|
||
|
|
partial=False,
|
||
|
|
).stream() as streamer:
|
||
|
|
async for message, btic_price in streamer:
|
||
|
|
message = json.loads(message)
|
||
|
|
|
||
|
|
if message["type"]=="subscribed": continue
|
||
|
|
assert message["type"]=="orderbook_snapshot"
|
||
|
|
|
||
|
|
mid, bids, asks = parse_orderbook_message(message)
|
||
|
|
|
||
|
|
if trans_hundred:
|
||
|
|
bids = convert_contract_hundred_lower(bids)
|
||
|
|
asks = convert_contract_hundred_lower(asks)
|
||
|
|
|
||
|
|
bid_shifed_lims = apply_btic_shift(lims[mid], btic_price, True)
|
||
|
|
ask_shifed_lims = apply_btic_shift(lims[mid], btic_price, False)
|
||
|
|
book[mid] = Digital(
|
||
|
|
*lims[mid],
|
||
|
|
bids = [conv_bid_to_pc(bid_shifed_lims, p, s, trans_hundred, is_index) for p, s in bids],
|
||
|
|
asks = [conv_ask_to_pc(ask_shifed_lims, p, s, trans_hundred, is_index) for p, s in asks],
|
||
|
|
exchange = self,
|
||
|
|
market_id = mid,
|
||
|
|
)
|
||
|
|
|
||
|
|
yield [*book.values()]
|
||
|
|
|
||
|
|
async def main():
|
||
|
|
async with aiohttp.ClientSession() as ks:
|
||
|
|
s = KalshiSession(ks)
|
||
|
|
await s.login()
|
||
|
|
await s.execute_order("05aed7ff-3506-4729-b78c-9e96c5b2f876", 2000, 1, False)
|
||
|
|
|
||
|
|
# async for book in s.orderbook_stream("INXW-22NOV04", convert_sp_rulebook):
|
||
|
|
# pprint.pprint(book)
|
||
|
|
# pass
|
||
|
|
|
||
|
|
|
||
|
|
if __name__=="__main__":
|
||
|
|
asyncio.run(main())
|
||
|
|
|