EventTrader
AI-Native Trading
PAPER
Menu
Dark Mode
Plain English Mode
PAPER TRADING MODE — Enable real trading on your Account page
Back

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)