Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,23 @@ prices = client.data.prices(["market-a", "market-b", "market-c"], days=30)
# Historical L2 orderbook
book = client.data.orderbook(
venue="polymarket",
market="will-btc-reach-100k",
start="2026-06-01T00:00:00Z",
end="2026-06-01T01:00:00Z",
market_id="will-btc-reach-100k",
limit=5000,
)

# Spot OHLCV candles (BTC, ETH, SOL)
klines = client.data.spot_klines(
symbol="BTC",
start="2026-06-01T00:00:00Z",
end="2026-06-01T01:00:00Z",
interval=300, # seconds
)

# Run a backtest on a thematic basket
result = client.backtests.run(
strategy_id="crypto_updown_roll_timing",
strategy_id="wsc_crypto_updown_delta_hedged_roll",
dataset_id="clickhouse:ito_hot.platform_orderbook_l2",
venues=["polymarket"],
date_range={"start": "2026-05-01T00:00:00Z", "end": "2026-06-01T00:00:00Z"},
Expand Down Expand Up @@ -84,23 +92,27 @@ print(f"P&L: ${result['data']['metrics']['pnl_usd']:.2f}")
| `client.markets.get(id)` | Single market detail |
| `client.markets.history(id)` | Daily price series |

### Research Data (2 endpoints)
### Research Data (3 endpoints)
| Method | Description |
|--------|-------------|
| `client.data.orderbook(venue, market)` | Historical L2 snapshots |
| `client.data.orderbook(venue, start, end)` | Historical L2 snapshots |
| `client.data.spot_klines(symbol, start, end)` | Spot OHLCV candles (BTC/ETH/SOL) |
| `client.data.prices(market_ids)` | Bulk daily close prices |

### Backtesting (9 endpoints)
### Backtesting (11 endpoints)
| Method | Description |
|--------|-------------|
| `client.backtests.strategies()` | Available strategies |
| `client.backtests.create_strategy(...)` | Create custom strategy |
| `client.backtests.custom_strategies()` | List custom strategies |
| `client.backtests.datasets()` | Available datasets |
| `client.backtests.execution_models()` | Fill models |
| `client.backtests.orderbook(start, end)` | Sample dataset L2 rows |
| `client.backtests.dataset(symbol, start, end)` | Assemble replay dataset |
| `client.backtests.validate(...)` | Dry-run validation |
| `client.backtests.plan(...)` | Multi-window experiment |
| `client.backtests.submit(...)` | Submit for execution |
| `client.backtests.get_result(run_id)` | Poll run status/result |
| `client.backtests.run(...)` | Submit + poll to completion |

## Features
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "ito-markets"
version = "0.1.0"
version = "0.1.1"
description = "Python SDK for the Ito Markets public API — prediction market data, baskets, and backtesting."
readme = "README.md"
license = "MIT"
Expand Down
7 changes: 6 additions & 1 deletion src/ito/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Ito Markets Python SDK — prediction market data, baskets, and backtesting."""

from importlib.metadata import PackageNotFoundError, version

from ito.client import ItoClient
from ito.exceptions import (
ItoAPIError,
Expand All @@ -18,4 +20,7 @@
"ItoValidationError",
]

__version__ = "0.1.0"
try:
__version__ = version("ito-markets")
except PackageNotFoundError: # running from a source tree without an install
__version__ = "0.0.0"
11 changes: 9 additions & 2 deletions src/ito/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import time
from importlib.metadata import PackageNotFoundError, version
from typing import Any

import httpx
Expand All @@ -15,11 +16,17 @@
ItoValidationError,
)

try:
_SDK_VERSION = version("ito-markets")
except PackageNotFoundError: # running from a source tree without an install
_SDK_VERSION = "0.0.0"

DEFAULT_BASE_URL = "https://itomarkets.com/api/v1"
DEFAULT_TIMEOUT = 30.0
MAX_RETRIES = 3
RETRY_BACKOFF_FACTOR = 1.0
RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
# 429 is handled by its own branch in _request, so it is intentionally absent here.
RETRYABLE_STATUS_CODES = {500, 502, 503, 504}


class HttpTransport:
Expand All @@ -40,7 +47,7 @@ def __init__(
base_url=self._base_url,
headers={
"Authorization": f"Bearer {api_key}",
"User-Agent": "ito-python/0.1.0",
"User-Agent": f"ito-python/{_SDK_VERSION}",
"Accept": "application/json",
},
timeout=timeout,
Expand Down
7 changes: 6 additions & 1 deletion src/ito/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ class ItoClient:
prices = client.markets.history("will-x-happen", days=90)

# Research data
orderbook = client.data.orderbook(venue="polymarket", market="will-x-happen")
orderbook = client.data.orderbook(
venue="polymarket",
start="2026-06-01T00:00:00Z",
end="2026-06-01T01:00:00Z",
market_id="will-x-happen",
)
bulk_prices = client.data.prices(["market-a", "market-b"], days=30)

# Backtesting
Expand Down
78 changes: 75 additions & 3 deletions src/ito/resources/backtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any

from ito._http import HttpTransport
from ito.exceptions import ItoAPIError


class Backtests:
Expand Down Expand Up @@ -122,6 +123,68 @@ def get_result(self, run_id: str) -> dict[str, Any]:
"""Get run status or result artifact (202 while running, 200 when complete)."""
return self._http.get(f"/backtests/atop/{run_id}")

def orderbook(
self,
start: str,
end: str,
source: str | None = None,
venue: str | None = None,
market_id: str | None = None,
limit: int = 5000,
offset: int = 0,
) -> dict[str, Any]:
"""Sample historical L2 rows from a registered dataset for inspection.

Args:
start: ISO 8601 start timestamp (required).
end: ISO 8601 end timestamp (required).
source: Dataset alias — 'platform_l2' or 'kalshi_pmxt' (default platform_l2).
venue: Optional venue filter.
market_id: Optional market identifier filter.
limit: Max rows (default 5000, max 50000).
offset: Pagination offset (default 0).
"""
return self._http.get(
"/backtests/atop/orderbook",
params={
"start": start,
"end": end,
"source": source,
"venue": venue,
"market_id": market_id,
"limit": limit,
"offset": offset,
},
)

def dataset(
self,
symbol: str,
start: str,
end: str,
interval: int = 300,
include_l2: bool = False,
) -> dict[str, Any]:
"""Assemble a backtest dataset (L2 windows + spot klines) for replay.

Args:
symbol: Asset symbol — 'BTC' or 'ETH'.
start: ISO 8601 start timestamp (required).
end: ISO 8601 end timestamp (required).
interval: Window size in seconds — 300 (5m) or 900 (15m).
include_l2: Include raw L2 book state (default False).
"""
return self._http.post(
"/backtests/atop/dataset",
json={
"symbol": symbol,
"start": start,
"end": end,
"interval": interval,
"include_l2": include_l2,
},
)

def run(
self,
strategy_id: str,
Expand All @@ -137,7 +200,8 @@ def run(
) -> dict[str, Any]:
"""Submit and poll until completion (convenience wrapper).

Returns the final result when status is 'succeeded' or 'failed'.
Returns the final result when the run status is 'succeeded'.
Raises ItoAPIError if the run ends in 'failed' or 'error'.
Raises TimeoutError if timeout exceeded.
"""
submission = self.submit(
Expand All @@ -159,11 +223,19 @@ def run(
raise TimeoutError(f"Backtest {run_id} did not complete within {timeout}s")

result = self.get_result(run_id)
status = result.get("data", {}).get("status", "")
data = result.get("data", {})
status = data.get("status", "")

if status in ("succeeded", "failed", "error"):
if status == "succeeded":
return result

if status in ("failed", "error"):
detail = data.get("error") or data.get("message") or "no detail provided"
raise ItoAPIError(
f"Backtest {run_id} ended with status {status!r}: {detail}",
response_body=result,
)

time.sleep(poll_interval)

@staticmethod
Expand Down
55 changes: 43 additions & 12 deletions src/ito/resources/data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Research data endpoints: orderbook snapshots, bulk prices."""
"""Research data endpoints: orderbook snapshots, spot klines, bulk prices."""

from __future__ import annotations

Expand All @@ -8,35 +8,66 @@


class Data:
"""Raw data access for quantitative research — orderbook L2 and bulk prices."""
"""Raw data access for quantitative research — orderbook L2, spot klines, bulk prices."""

def __init__(self, http: HttpTransport) -> None:
self._http = http

def orderbook(
self,
venue: str,
market: str,
start: str | None = None,
end: str | None = None,
limit: int = 1000,
start: str,
end: str,
market_id: str | None = None,
limit: int = 5000,
offset: int = 0,
) -> dict[str, Any]:
"""Historical L2 orderbook snapshots (max 24h window per request).
"""Historical L2 orderbook snapshots (max 168h / 7-day window per request).

Args:
venue: One of 'polymarket', 'kalshi', 'hyperliquid'.
market: Market identifier / ticker.
start: ISO 8601 start time (default: 1 hour ago).
end: ISO 8601 end time (default: now).
limit: Max rows (default 1000, max 10000).
start: ISO 8601 start timestamp (required).
end: ISO 8601 end timestamp (required).
market_id: Optional filter by market identifier (condition_id, ticker).
limit: Max rows (default 5000, max 50000).
offset: Pagination offset (default 0).
"""
return self._http.get(
"/data/orderbook",
params={
"venue": venue,
"market": market,
"start": start,
"end": end,
"market_id": market_id,
"limit": limit,
"offset": offset,
},
)

def spot_klines(
self,
symbol: str,
start: str,
end: str,
interval: int = 300,
limit: int = 5000,
) -> dict[str, Any]:
"""Historical spot OHLCV candles for major cryptoassets.

Args:
symbol: Asset symbol — 'BTC', 'ETH', or 'SOL'.
start: ISO 8601 start timestamp (required).
end: ISO 8601 end timestamp (required).
interval: Candle interval in seconds, 60–86400 (default 300 = 5m).
limit: Max rows (default 5000, max 50000).
"""
return self._http.get(
"/data/spot/klines",
params={
"symbol": symbol,
"start": start,
"end": end,
"interval": interval,
"limit": limit,
},
)
Expand Down
Loading
Loading