Horse Race Python SDK
Thin, dependency-light Python client for Horse Race placement markets. Each race is a field of N assets (crypto or stock); each (asset, position) pair has its own CLOB. The SDK wraps the same endpoints documented in the REST API reference.
Install
pip install requests websockets
The SDK is a single file. Drop it in your project — no package install required.
Quickstart
from horse_race import HorseRaceClient
client = HorseRaceClient(
base_url="https://cymetica.com",
email="you@example.com",
password="…",
)
# List active races
races = client.races()
race = races[0]
print(f"Race {race['id']}: {len(race['assets'])} assets, max placement {race['max_placement']}")
# Best bid/ask matrix across the whole race.
# The endpoint returns {"grid": {asset: {position: {...}}}, "row_sums": ..., "col_sums": ..., "field_size", "max_placement"}.
g = client.grid(race["id"])
matrix = g["grid"] # actual asset → position → {best_bid, best_ask, mid_price, last_trade}
row_sums = g["row_sums"] # per-asset (should approach 1.0 in efficient markets)
# Read the BTC-finishes-1st orderbook
book = client.orderbook(race["id"], "BTC", 1)
print("best_bid:", book["best_bid"], "best_ask:", book["best_ask"])
# Buy 10 shares of "BTC finishes 1st" at $0.35
order = client.place_order(
race_id=race["id"],
asset="BTC",
position=1,
side="buy",
price=0.35,
size=10,
)
Browse Fields
# Find all crypto races
races = client.races()
crypto = [r for r in races if any(a["asset_type"] == "crypto" for a in r["assets"])]
stocks = [r for r in races if any(a["asset_type"] == "stock" for a in r["assets"])]
print(f"Crypto races: {len(crypto)}, Stock races: {len(stocks)}")
Streaming a Race
The WebSocket emits messages with a type field. Most data is flat on the message itself (no "data" wrapper).
import asyncio
from horse_race import HorseRaceStream
async def main():
race = client.races()[0]
async with HorseRaceStream(race_id=race["id"]) as stream:
async for event in stream:
t = event.get("type")
if t == "placement_trade":
# {"type":"placement_trade","asset":"BTC","position":1,"trade":{"price":..,"size":..,"side":..}}
tr = event["trade"]
print(f"{event['asset']} pos={event['position']} "
f"{tr.get('side')} {tr['size']}@{tr['price']}")
elif t == "depth":
# {"type":"depth","asset":"BTC","position":1,"depth":{"best_bid":..,"best_ask":..,...}}
d = event["depth"]
print(f"{event['asset']} pos={event['position']} "
f"bid={d.get('best_bid')} ask={d.get('best_ask')}")
elif t == "grid_update":
print("grid updated:", list(event["grid"].keys()))
elif t == "race_result":
print("RESOLVED:", event["results"])
asyncio.run(main())
SDK Source (paste this file)
"""horse_race.py — minimal client for EventTrader Horse Race placement markets."""
import json
import requests
import websockets
BASE_PATH = "/api/v1/horse-race"
class HorseRaceClient:
def __init__(self, base_url: str, email: str | None = None, password: str | None = None):
self.base = base_url.rstrip("/")
self.s = requests.Session()
self.token = None
if email and password:
r = self.s.post(f"{self.base}/auth/login",
json={"email": email, "password": password}, timeout=15)
r.raise_for_status()
self.token = r.json()["access_token"]
self.s.headers.update({"Authorization": f"Bearer {self.token}"})
def races(self, active_only: bool = True):
r = self.s.get(f"{self.base}{BASE_PATH}/races",
params={"active_only": str(active_only).lower()}, timeout=10)
r.raise_for_status()
return r.json()
def race(self, race_id: str):
r = self.s.get(f"{self.base}{BASE_PATH}/races/{race_id}", timeout=10)
r.raise_for_status()
return r.json()
def orderbook(self, race_id: str, asset: str, position: int):
r = self.s.get(f"{self.base}{BASE_PATH}/races/{race_id}/book/{asset}/{position}", timeout=10)
r.raise_for_status()
return r.json()
def books(self, race_id: str):
r = self.s.get(f"{self.base}{BASE_PATH}/races/{race_id}/books", timeout=10)
r.raise_for_status()
return r.json()
def grid(self, race_id: str):
r = self.s.get(f"{self.base}{BASE_PATH}/races/{race_id}/grid", timeout=10)
r.raise_for_status()
return r.json()
def place_order(self, race_id: str, asset: str, position: int,
side: str, price: float, size: float,
order_type: str = "limit", post_only: bool = False):
r = self.s.post(
f"{self.base}{BASE_PATH}/races/{race_id}/orders",
json={"asset_symbol": asset, "position": position, "side": side,
"price": price, "size": size,
"order_type": order_type, "post_only": post_only},
timeout=15,
)
r.raise_for_status()
return r.json()
def cancel_order(self, race_id: str, order_id: str, asset: str, position: int):
# asset_symbol and position are REQUIRED query params — the engine routes by them.
r = self.s.delete(
f"{self.base}{BASE_PATH}/races/{race_id}/orders/{order_id}",
params={"asset_symbol": asset, "position": position},
timeout=10,
)
r.raise_for_status()
return r.json()
def my_positions(self, race_id: str):
r = self.s.get(f"{self.base}{BASE_PATH}/races/{race_id}/my-positions", timeout=10)
r.raise_for_status()
return r.json()
def my_orders(self, race_id: str):
r = self.s.get(f"{self.base}{BASE_PATH}/races/{race_id}/orders", timeout=10)
r.raise_for_status()
return r.json()
def results(self, race_id: str):
r = self.s.get(f"{self.base}{BASE_PATH}/races/{race_id}/results", timeout=10)
r.raise_for_status()
return r.json()
class HorseRaceStream:
def __init__(self, race_id: str, base_url: str = "wss://cymetica.com"):
self.url = f"{base_url}/ws/horse-race/{race_id}"
self._ws = None
async def __aenter__(self):
self._ws = await websockets.connect(self.url)
return self
async def __aexit__(self, *a):
if self._ws:
await self._ws.close()
def __aiter__(self):
return self
async def __anext__(self):
msg = await self._ws.recv()
return json.loads(msg)
Examples
Spread scanner — find the widest podium spreads
race = client.races()[0]
g = client.grid(race["id"])
matrix = g["grid"] # asset → position(str) → {best_bid, best_ask, mid_price, last_trade}
widest = []
for asset, by_pos in matrix.items():
for pos_str, lvl in by_pos.items():
bid, ask = lvl.get("best_bid"), lvl.get("best_ask")
if bid is not None and ask is not None:
widest.append((float(ask) - float(bid), asset, int(pos_str), bid, ask))
widest.sort(reverse=True)
for spread, asset, pos, bid, ask in widest[:10]:
print(f"{asset} pos={pos} {bid}/{ask} spread={spread:.3f}")
Stream live fills for one race
import asyncio
async def watch(race_id):
async with HorseRaceStream(race_id) as stream:
async for event in stream:
if event.get("type") == "placement_trade":
tr = event["trade"]
print(f"{event['asset']} pos={event['position']} "
f"{tr.get('side')} {tr['size']}@{tr['price']}")
asyncio.run(watch(client.races()[0]["id"]))
Track resolution + payouts
# After a race resolves, fetch the final placements and your settlement.
race_id = client.races(active_only=False)[0]["id"]
res = client.results(race_id)
if res.get("message") == "Race not yet resolved":
print("still trading")
else:
for r in res["results"]:
print(f"final pos {r['final_position']}: {r['asset_symbol']}")
my = client.my_orders(race_id)
print("filled orders:", [o for o in my if o["status"] == "filled"])
Buy-the-underdog bot — bid for the cheapest 1st-place share
race = client.races()[0]
race_id = race["id"]
matrix = client.grid(race_id)["grid"] # unwrap the outer {grid: ..., row_sums, col_sums, ...}
# Lowest best_ask across all assets for position 1
candidates = []
for asset, by_pos in matrix.items():
cell = by_pos.get("1") or {}
if cell.get("best_ask") is not None:
candidates.append((float(cell["best_ask"]), asset))
if candidates:
candidates.sort()
ask, asset = candidates[0]
# Bid one cent below the ask, size 5
client.place_order(race_id, asset, position=1, side="buy",
price=round(ask - 0.01, 2), size=5, post_only=True)