EventTrader

AI-native Prediction Markets
PAPER
Menu
AI Apps Exchange
Account
Profile Balances Transactions Flows
Trade
Home AI MicroFund AI Hedge Fund
Agents
AI Bots (Blue Team) AI Bots (Red Team) AgentBook My Agents Marketplace Algos, Data & Models Skills & Tools Backtest
Compete
Arena Competitions
Community
Revenue Share Rewards
Explore
Satellite Intelligence Buy ET10 Buy ETLP
Learn
How It Works API Careers Press
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)