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.

317 lines
11 KiB

3 weeks ago
import base64
import aiohttp
import websockets
import os
import hmac
import asyncio
import operator
from decimal import Decimal as D
import dill as pickle
import hashlib
import json
import requests
import datetime
import time
from utils import *
from piecewise import *
TRADOVATE_AUTH = read_auth("tradovate")
TRADOVATE_SESSION_FILE = ".tradovate_session"
TRADOVATE_DEVICEID = "b21da153-4e25-4679-b958-053cc5dc8eeb"
# My Own: "76fdc8d9-e156-46a4-b2b2-187087fcd35e"
TRADOVATE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
# My Own: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
TRADOVATE_ROOT = "https://demo.tradovateapi.com/v1" if DEMO else "https://live.tradovateapi.com/v1"
TRADOVATE_LIVE_ROOT = "https://live.tradovateapi.com/v1"
TRADOVATE_WS = "wss://demo.tradovateapi.com/v1/websocket" if DEMO else "wss://live.tradovateapi.com/v1/websocket"
TRADOVATE_MD_WS = "wss://md-demo.tradovateapi.com/v1/websocket" if DEMO else "wss://md.tradovateapi.com/v1/websocket"
TRADOVATE_APPID = "tradovate_trader(web)"
TRADOVATE_ORIGIN = "https://trader.tradovate.com"
COMMON_HEADERS = {
"accept": "*/*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.9",
"cache-control": "no-cache",
"origin": TRADOVATE_ORIGIN,
"pragma": "no-cache",
"referer": TRADOVATE_ORIGIN+"/",
"user-agent": TRADOVATE_USER_AGENT,
}
def conv_ask_to_pc(lims, p, s):
# TODO: make sure this is correct
fee = 11 * 5
return (conv_lim_pc(*lims) + (-p - fee), s, p)
def convert_contract_hundred_lower(data):
rem = 0
ret = []
for p, s in data:
s += rem
if s//5: ret.append((p*5, s//5))
rem = s%5
return ret
class TradovateSocket:
# TODO: figure out where the weird r query param comes from
def __init__(self, url, cookie_jar):
self.context = websockets.connect(
url,
extra_headers = {
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Cache-Control": "no-cache",
"Connection": "Upgrade",
"Pragma": "no-cache",
"Upgrade": "websocket",
"User-Agent": TRADOVATE_USER_AGENT,
"Cookie": cookie_jar.filter_cookies(url).output(header=''),
},
origin = TRADOVATE_ORIGIN,
)
self.id = 1
self.cur_time = time.time()
async def __aenter__(self):
self.ws = await self.context.__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
await self.context.__aexit__(*args, **kwargs)
async def send(self, op, query="", body=""):
msg = "%s\n%d\n%s\n%s" % (op, self.id, query, body)
self.id += 1
await self.ws.send(msg)
async def stream(self):
async for msg in self.ws:
print(msg)
now = time.time()
if now - self.cur_time > 2.5:
await self.ws.send('[]')
self.cur_time = now
if msg[0]!='a': continue
yield msg[1:]
class TradovateSession:
def __init__(self, s):
self.s = s
self.ticker_lookup = {}
def __repr__(self):
return "tradovate"
@staticmethod
def make_login_payload(username, password):
ts = str(int(time.time()*1000 - 1581e9))
sec = hmac.new(
"035a1259-11e7-485a-aeae-9b6016579351".encode(),
(ts + TRADOVATE_DEVICEID + username + password + TRADOVATE_APPID).encode(),
digestmod = hashlib.sha256
).hexdigest()
return {
"name": username,
"password": password,
"appId": TRADOVATE_APPID,
"appVersion": "1.221209.1",
"deviceId": TRADOVATE_DEVICEID,
# "userAgent": "Dart/2.17.6; ec/1.221209.0", # This depends on version too
"environment": "demo",
"cid": 1, # This is client id
"sec": sec,
"chl": ts,
}
# Making descision not to allow not to cache logins
# a) They have limited logins allowed, so until we can detect auth fail, this would be bad
# b) EC seems to have short login window anyways
# Still should allow it for development work when I'm slamming them
# TODO: renew these tokens, expiry is every 80 min
async def login(self, use_cached=False):
self.s.headers.clear()
self.s.headers.update(COMMON_HEADERS)
self.s.cookie_jar.clear()
if use_cached and os.path.exists(TRADOVATE_SESSION_FILE):
with open(TRADOVATE_SESSION_FILE, 'rb') as f:
self.user_id, self.access_token, self.mdaccess_token, cookies = pickle.load(f)
self.s.headers['authorization'] = "Bearer " + self.access_token
self.s.cookie_jar._cookies = cookies
else:
print("doing new login")
async with self.s.post(
TRADOVATE_LIVE_ROOT + "/auth/accesstokenrequest",
data = json.dumps(TradovateSession.make_login_payload(**TRADOVATE_AUTH)),
headers = {
"content-type": "text/plain; charset=utf-8",
},
) as resp:
data = await resp.json()
self.access_token = data["accessToken"]
self.mdaccess_token = data["mdAccessToken"]
self.user_id = data["userId"]
self.s.headers['authorization'] = "Bearer " + self.access_token
with open(TRADOVATE_SESSION_FILE, 'wb') as f:
pickle.dump((self.user_id, self.access_token, self.mdaccess_token, self.s.cookie_jar._cookies), f)
async def get_ldep(self):
async with self.s.get(
TRADOVATE_ROOT + "/account/ldeps",
params = {'masterids': self.user_id},
) as resp:
data = await resp.json()
self.ldep_id = data[0]['id']
async def setup_ordering(self):
await self.get_ldep()
self.order_sock = TradovateSocket(TRADOVATE_WS, self.s.cookie_jar)
await self.order_sock.__aenter__()
await self.order_sock.send("authorize", body=self.access_token)
async def order_ws_log():
async for msg in self.order_sock.stream():
print("ORDER LOG: " + msg)
print()
asyncio.create_task(order_ws_log())
## Only myster here is tickers and what accountId is in this case
# user/registeraudituseraction
# 209
#
# {"accountId":1368262,"actionType":"OrderTicketBuy","details":"ORDERTICKET OECES27Z2 C38000: Order Ticket Buy, Buy 3 Limit 15.00, TIF Day"}
#
#
# order/placeorder
# 210
#
# {"accountId":1368262,"action":"Buy","symbol":"OECES27Z2 C38000","orderQty":3,"orderType":"Limit","price":15,"timeInForce":"Day","text":"Ticket"}
async def execute_order(self, mid, p, s, buy):
p = D(p/5)/100
s *= 5
assert buy
name = self.ticker_lookup[mid]
audit_msg = {
"accountId": self.ldep_id,
"actionType": "OrderTicketBuy",
"details": "ORDERTICKET %s: Order Ticket Buy, Buy %d Limit %0.2f, TIF Day"
% (name, s, p),
}
order_msg = {
"accountId": self.ldep_id,
"action": "Buy",
"symbol": name,
"orderQty": s,
"orderType": "Limit",
"price": float(p),
"timeInForce": "Day",
"text": "Ticket",
}
print(json.dumps(audit_msg))
print(json.dumps(order_msg))
await self.order_sock.send("user/registeraudituseraction", body=json.dumps(audit_msg))
await self.order_sock.send("order/placeorder", body=json.dumps(order_msg))
async def markets_from_maturity(self, maturityid):
async with self.s.get(
TRADOVATE_ROOT + "/contract/deps",
params = {'masterid': maturityid},
) as resp:
data = await resp.json()
lims = {}
for contract in data:
mid = contract['id']
if mid%10!=5: continue
strike = cents(contract['strikePrice'])
put = contract['isPut']
self.ticker_lookup[mid] = contract['name']
# TODO: figure out boundary
if put:
lims[mid] = (None, strike)
else:
lims[mid] = (strike, None)
print(lims)
return lims
async def orderbook_stream(self, maturityid):
lims = await self.markets_from_maturity(maturityid)
book = {}
async with TradovateSocket(TRADOVATE_MD_WS, self.s.cookie_jar) as ws:
await ws.send("authorize", body=self.mdaccess_token)
for symbol in lims:
await ws.send("md/subscribedom", body='{"symbol":"%d"}' %symbol)
async for msg in ws.stream():
data = json.loads(msg)
doms = []
for ev in data:
if ev.get('e')!='md': continue
for dom_ev in ev.get('d', {}).get('doms', []):
ts = datetime.datetime.fromisoformat(dom_ev['timestamp'][:-1])
doms.append((ts, dom_ev))
if not doms: continue
doms.sort(key=operator.itemgetter(0))
for ts, upd in doms:
mid = upd['contractId']
prices = [(cents(o['price']), o['size']) for o in upd['offers']]
prices.sort()
prices = convert_contract_hundred_lower(prices)
lim = lims[mid]
book[mid] = Digital(
*lim,
bids = [],
asks = [conv_ask_to_pc(lim, p, s) for p, s in prices],
exchange = self,
market_id = mid,
)
print(book[mid])
if prices:
await self.execute_order(mid, book[mid].asks[0][-1], 4, True)
import sys
sys.exit(0)
yield [*book.values()]
async def main():
# print(TradovateSession.make_login_payload(**TRADOVATE_AUTH))
# return
async with aiohttp.ClientSession() as ts:
s = TradovateSession(ts)
await s.login(use_cached=True)
await s.setup_ordering()
async for book in s.orderbook_stream(maturityid=51047):
print(book)
# await asyncio.sleep(10000)
# print(await s.test_req())
if __name__=="__main__":
asyncio.run(main())