From 9559a9e3bc46f4ac93e4093db6f74f1b6cc5f260 Mon Sep 17 00:00:00 2001 From: Brian Hartford Date: Sat, 24 Jan 2026 14:30:00 -0500 Subject: [PATCH 1/3] Add dark logo --- README.md | 9 ++++++--- assets/logo-light-bg.png | Bin 0 -> 35432 bytes 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 assets/logo-light-bg.png diff --git a/README.md b/README.md index 1be14f0..431c528 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@

- Kyro + + + Kyro +

# Kyro -[![Ruff](https://github.com/UTXOnly/kyro/actions/workflows/ruff.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/ruff.yml) [![Black](https://github.com/UTXOnly/kyro/actions/workflows/black.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/black.yml) [![Tests](https://github.com/UTXOnly/kyro/actions/workflows/test.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/test.yml) [![Benchmarks](https://github.com/UTXOnly/kyro/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/benchmarks.yml) +[![PyPI](https://img.shields.io/pypi/v/kyro.svg)](https://pypi.org/project/kyro/) [![Ruff](https://github.com/UTXOnly/kyro/actions/workflows/ruff.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/ruff.yml) [![Black](https://github.com/UTXOnly/kyro/actions/workflows/black.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/black.yml) [![Tests](https://github.com/UTXOnly/kyro/actions/workflows/test.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/test.yml) [![Benchmarks](https://github.com/UTXOnly/kyro/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/UTXOnly/kyro/actions/workflows/benchmarks.yml) Kyro is an async Python client library for the Kalshi REST API. @@ -31,7 +34,7 @@ Errors are surfaced as explicit exception types: `KyroError` (base), `KyroHTTPEr ## Install -From PyPI (after a release): +From [PyPI](https://pypi.org/project/kyro/): ```bash pip install kyro diff --git a/assets/logo-light-bg.png b/assets/logo-light-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..7c2c9209137d13c642816951a0d50889728ba60b GIT binary patch literal 35432 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 Date: Sat, 24 Jan 2026 19:26:39 -0500 Subject: [PATCH 2/3] Add new REST API methods --- API_REFERENCE.md | 78 +++++++++++++++++++++++++++++++++- README.md | 28 +++++++++--- pyproject.toml | 2 +- scripts/live_api_smoke.py | 28 +++++++++++- src/kyro/_version.py | 2 +- src/kyro/rest/__init__.py | 5 ++- src/kyro/rest/api/__init__.py | 6 +-- src/kyro/rest/api/events.py | 37 ++++++++++++++++ src/kyro/rest/api/portfolio.py | 9 ++++ src/kyro/rest/api/search.py | 27 ++++++++++++ 10 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 src/kyro/rest/api/search.py diff --git a/API_REFERENCE.md b/API_REFERENCE.md index 0b4d8d6..f65f926 100644 --- a/API_REFERENCE.md +++ b/API_REFERENCE.md @@ -1,8 +1,8 @@ # Kyro API Reference -Request/response documentation for every modular method: **exchange**, **markets**, **events**, **orders**, **portfolio**. +Request/response documentation for every modular method: **exchange**, **markets**, **events**, **orders**, **portfolio**, **search**. -- **Import:** `from kyro.rest import exchange, markets, events, orders, portfolio` +- **Import:** `from kyro.rest import exchange, markets, events, orders, portfolio, search` - **Client:** Pass `RestClient` as the first argument: `await exchange.get_exchange_status(client)` - **Base path:** `{KyroConfig.base_url}` (e.g. `https://api.elections.kalshi.com/trade-api/v2`) - **Auth:** Endpoints marked *Auth required* need `KyroConfig(auth_headers={...})` with KALSHI-ACCESS-KEY, TIMESTAMP, SIGNATURE. @@ -532,6 +532,32 @@ data = await events.get_event_metadata(client, "KXBTC") --- +### `get_event_candlesticks` + +**HTTP:** `GET /series/{series_ticker}/events/{event_ticker}/candlesticks` +**Auth:** No + +**Usage:** +```python +data = await events.get_event_candlesticks( + client, + "KXBTC", + "KXBTC-24JAN15", + start_ts=1704067200, + end_ts=1704153600, + period_interval=60, + limit=24, +) +``` + +If `start_ts`/`end_ts` omitted, uses last 24h. + +**Query:** `start_ts`, `end_ts` (Unix), `period_interval` (1|60|1440 minutes), `limit`, `include_latest_before_start` + +**Response (200):** `{ "candlesticks": [ { "start_ts", "end_ts", "open", "high", "low", "close", "volume" }, ... ] }` per Kalshi + +--- + ### `get_multivariate_events` **HTTP:** `GET /events/multivariate` @@ -553,6 +579,40 @@ data = await events.get_multivariate_events(client, limit=100, cursor=None) --- +## Search + +No auth unless noted. + +--- + +### `get_sports_filters` + +**HTTP:** `GET /search/filters_by_sport` +**Auth:** No + +**Usage:** +```python +data = await search.get_sports_filters(client) +``` + +**Response (200):** sport-based filter metadata for search/discovery (structure per Kalshi) + +--- + +### `get_tags_by_categories` + +**HTTP:** `GET /search/tags_by_categories` +**Auth:** No + +**Usage:** +```python +data = await search.get_tags_by_categories(client) +``` + +**Response (200):** tags grouped by category for search/filtering (structure per Kalshi) + +--- + ## Orders All order endpoints **require auth**. @@ -856,6 +916,20 @@ All portfolio endpoints **require auth**. --- +### `get_portfolio` + +**HTTP:** `GET /portfolio` +**Auth:** Yes + +**Usage:** +```python +data = await portfolio.get_portfolio(client) +``` + +**Response (200):** portfolio summary (structure per Kalshi). If the endpoint is not available for an account, use `get_balance`, `get_positions`, `get_fills` instead. + +--- + ### `get_balance` **HTTP:** `GET /portfolio/balance` diff --git a/README.md b/README.md index 431c528..93de870 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ API areas are grouped into: - `exchange` - `markets` - `events` +- `search` - `orders` - `portfolio` @@ -174,7 +175,7 @@ async with RestClient(cfg) as client: | Requires auth | Endpoints | |---------------|-----------| -| **No** | `exchange.get_exchange_status`, `get_exchange_announcements`, `get_exchange_schedule`, `get_series_fee_changes`; all of `markets.*` and `events.*` | +| **No** | `exchange.get_exchange_status`, `get_exchange_announcements`, `get_exchange_schedule`, `get_series_fee_changes`; all of `markets.*`, `events.*`, and `search.*` | | **Yes** | `exchange.get_user_data_timestamp`; all of `orders.*` and `portfolio.*` | Without auth, public endpoints work as usual. Auth-required calls return `401` if the headers are missing or invalid. @@ -189,18 +190,19 @@ Without auth, public endpoints work as usual. Auth-required calls return `401` i --- -## Modular API (exchange, markets, events, orders, portfolio) +## Modular API (exchange, markets, events, search, orders, portfolio) ```python from kyro import RestClient, KyroConfig -from kyro.rest import exchange, markets, events, orders, portfolio +from kyro.rest import exchange, markets, events, search, orders, portfolio async with RestClient(KyroConfig()) as client: - # Exchange (no auth) + # Exchange (no auth except get_user_data_timestamp) status = await exchange.get_exchange_status(client) await exchange.get_exchange_announcements(client) await exchange.get_exchange_schedule(client) await exchange.get_series_fee_changes(client, series_ticker="KXBTC") + await exchange.get_user_data_timestamp(client) # auth # Markets — filters: series_ticker, event_ticker, status, tickers, min/max_*_ts, cursor ms = await markets.get_markets( @@ -225,6 +227,8 @@ async with RestClient(KyroConfig()) as client: period_interval=60, limit=100, ) + await markets.get_live_data(client, "KXBTC-24JAN15") + await markets.get_multiple_live_data(client, "KXBTC-24JAN15,INXD-25") await markets.get_series(client, "KXBTC") await markets.get_series_list(client, limit=20) # cursor= for pagination @@ -238,8 +242,15 @@ async with RestClient(KyroConfig()) as client: ) ev = await events.get_event(client, "INXD-25", with_nested_markets=True) await events.get_event_metadata(client, "INXD-25") + await events.get_event_candlesticks( + client, "KXBTC", "INXD-25", period_interval=60, limit=100 + ) await events.get_multivariate_events(client, limit=10) + # Search (no auth) + await search.get_sports_filters(client) + await search.get_tags_by_categories(client) + # Orders (auth) — filters: ticker, event_ticker, status, min_ts, max_ts, cursor, subaccount ords = await orders.get_orders( client, ticker="KXBTC-24JAN15", status="resting", limit=50 @@ -258,13 +269,15 @@ async with RestClient(KyroConfig()) as client: await orders.amend_order( client, "order-id", ticker="KXBTC-24JAN15", side="yes", action="buy", yes_price=55 ) + await orders.decrease_order(client, "order-id", reduce_by=1) await orders.batch_create_orders( client, [{"ticker": "KXBTC-24JAN15", "side": "yes", "action": "buy", "count": 1, "yes_price": 50}], ) - await orders.batch_cancel_orders(client, ids=["id1", "id2"]) + await orders.batch_cancel_orders(client, order_ids=["id1", "id2"]) # Portfolio (auth) — filters: ticker, event_ticker, min_ts, max_ts, cursor, subaccount + await portfolio.get_portfolio(client) bal = await portfolio.get_balance(client) pos = await portfolio.get_positions( client, ticker="KXBTC-24JAN15", limit=100 @@ -286,7 +299,7 @@ async with RestClient(KyroConfig()) as client: ## API Reference -Full request/response docs for **every method** (exchange, markets, events, orders, portfolio): +Full request/response docs for **every method** (exchange, markets, events, search, orders, portfolio): **[API_REFERENCE.md](API_REFERENCE.md)** --- @@ -413,12 +426,13 @@ kyro/ │ ├── _version.py │ ├── exceptions.py # KyroError, KyroHTTPError, KyroTimeoutError, KyroConnectionError, KyroValidationError │ └── rest/ -│ ├── __init__.py # RestClient, exchange, markets, events, orders, portfolio +│ ├── __init__.py # RestClient, exchange, markets, events, search, orders, portfolio │ ├── client.py │ └── api/ │ ├── exchange.py │ ├── markets.py │ ├── events.py +│ ├── search.py │ ├── orders.py │ └── portfolio.py ├── benchmarks/ # pytest-benchmark: serialization, REST client vs local mock diff --git a/pyproject.toml b/pyproject.toml index 57eef99..fe247b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "kyro" -version = "0.1.1" +version = "0.1.2" description = "Async Kalshi API client (aiohttp, Pydantic). Library for building apps." readme = "README.md" license = { text = "MIT" } diff --git a/scripts/live_api_smoke.py b/scripts/live_api_smoke.py index 7940120..6251a19 100644 --- a/scripts/live_api_smoke.py +++ b/scripts/live_api_smoke.py @@ -42,7 +42,7 @@ from kyro import RestClient, config_from_env from kyro.exceptions import KyroConnectionError, KyroHTTPError, KyroTimeoutError -from kyro.rest import events, exchange, markets, orders, portfolio +from kyro.rest import events, exchange, markets, orders, portfolio, search AUDIT_LOG_ENV = "KALSHI_SMOKE_AUDIT_LOG" DEFAULT_AUDIT_LOG = "live_smoke_audit.log" @@ -71,6 +71,11 @@ ("orders", "batch_create_orders"): "POST /portfolio/orders/batched", ("orders", "batch_cancel_orders"): 'DELETE /portfolio/orders/batched body {"ids":[...]}', ("orders", "cancel_order"): "DELETE /portfolio/orders/{order_id}", + ( + "events", + "get_event_candlesticks", + ): "GET /series/{series_ticker}/events/{event_ticker}/candlesticks?start_ts=&end_ts=&period_interval=1|60|1440", + ("portfolio", "get_portfolio"): "GET /portfolio (may 404 for some accounts)", } @@ -230,7 +235,10 @@ async def _get_events(client: RestClient, ctx: dict) -> Any: r = await events.get_events(client, limit=5) evs = (r or {}).get("events") or [] if evs: - ctx["event_ticker"] = evs[0].get("event_ticker") + e = evs[0] + ctx["event_ticker"] = e.get("event_ticker") + if e.get("series_ticker") and not ctx.get("series_ticker"): + ctx["series_ticker"] = e["series_ticker"] return r @@ -339,6 +347,8 @@ async def _run_and_log( ("exchange", "get_exchange_schedule", lambda c, x: exchange.get_exchange_schedule(c), {}), ("exchange", "get_series_fee_changes", lambda c, x: exchange.get_series_fee_changes(c), {}), ("exchange", "get_user_data_timestamp", lambda c, x: exchange.get_user_data_timestamp(c), {}), + ("search", "get_sports_filters", lambda c, x: search.get_sports_filters(c), {}), + ("search", "get_tags_by_categories", lambda c, x: search.get_tags_by_categories(c), {}), ("events", "get_events", _get_events, {"limit": 5}), ( "events", @@ -352,6 +362,19 @@ async def _run_and_log( lambda c, x: events.get_event_metadata(c, _event_ticker(x)), lambda ctx: {"event_ticker": _event_ticker(ctx)}, ), + ( + "events", + "get_event_candlesticks", + lambda c, x: events.get_event_candlesticks( + c, _series_ticker(x), _event_ticker(x), limit=5, period_interval=60 + ), + lambda ctx: { + "series_ticker": _series_ticker(ctx), + "event_ticker": _event_ticker(ctx), + "limit": 5, + "period_interval": 60, + }, + ), ( "events", "get_multivariate_events", @@ -405,6 +428,7 @@ async def _run_and_log( lambda ctx: {"tickers": _ticker(ctx)}, ), ("orders", "get_orders", lambda c, x: orders.get_orders(c, limit=5), {"limit": 5}), + ("portfolio", "get_portfolio", lambda c, x: portfolio.get_portfolio(c), {}), ("portfolio", "get_balance", lambda c, x: portfolio.get_balance(c), {}), ("portfolio", "get_positions", lambda c, x: portfolio.get_positions(c, limit=5), {"limit": 5}), ("portfolio", "get_fills", lambda c, x: portfolio.get_fills(c, limit=5), {"limit": 5}), diff --git a/src/kyro/_version.py b/src/kyro/_version.py index 0a6cfe5..af5f41c 100644 --- a/src/kyro/_version.py +++ b/src/kyro/_version.py @@ -1,3 +1,3 @@ """Package version.""" -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/src/kyro/rest/__init__.py b/src/kyro/rest/__init__.py index 47c47ec..e1c1fba 100644 --- a/src/kyro/rest/__init__.py +++ b/src/kyro/rest/__init__.py @@ -1,6 +1,6 @@ -"""REST client and modular Kalshi API (exchange, markets, events, orders, portfolio).""" +"""REST client and modular Kalshi API (exchange, markets, events, orders, portfolio, search).""" -from kyro.rest.api import events, exchange, markets, orders, portfolio +from kyro.rest.api import events, exchange, markets, orders, portfolio, search from kyro.rest.client import RestClient __all__ = [ @@ -10,4 +10,5 @@ "markets", "orders", "portfolio", + "search", ] diff --git a/src/kyro/rest/api/__init__.py b/src/kyro/rest/api/__init__.py index a6e20ca..6d97af8 100644 --- a/src/kyro/rest/api/__init__.py +++ b/src/kyro/rest/api/__init__.py @@ -2,12 +2,12 @@ Example: >>> from kyro import RestClient, KyroConfig - >>> from kyro.rest import exchange, markets, events, orders, portfolio + >>> from kyro.rest import exchange, markets, events, orders, portfolio, search >>> async with RestClient(KyroConfig()) as client: ... status = await exchange.get_exchange_status(client) ... ms = await markets.get_markets(client, limit=10) """ -from . import events, exchange, markets, orders, portfolio +from . import events, exchange, markets, orders, portfolio, search -__all__ = ["exchange", "events", "markets", "orders", "portfolio"] +__all__ = ["exchange", "events", "markets", "orders", "portfolio", "search"] diff --git a/src/kyro/rest/api/events.py b/src/kyro/rest/api/events.py index d3149ec..5a77738 100644 --- a/src/kyro/rest/api/events.py +++ b/src/kyro/rest/api/events.py @@ -5,6 +5,7 @@ from __future__ import annotations +import time from typing import TYPE_CHECKING, Any if TYPE_CHECKING: @@ -61,6 +62,42 @@ async def get_event_metadata(client: RestClient, event_ticker: str) -> Any: return await client.get(f"/events/{event_ticker}/metadata") +async def get_event_candlesticks( + client: RestClient, + series_ticker: str, + event_ticker: str, + *, + start_ts: int | None = None, + end_ts: int | None = None, + period_interval: int | None = None, + limit: int | None = None, + include_latest_before_start: bool | None = None, +) -> Any: + """Get OHLCV candlesticks for an event. `GET /series/{series_ticker}/events/{event_ticker}/candlesticks`. + + start_ts, end_ts (Unix). period_interval: 1, 60, or 1440 (minutes). + If start_ts/end_ts omitted, uses last 24h. limit 1–1000. + """ + now = int(time.time()) + if start_ts is None: + start_ts = now - 86400 + if end_ts is None: + end_ts = now + params = _clean( + { + "start_ts": start_ts, + "end_ts": end_ts, + "period_interval": period_interval, + "limit": limit, + "include_latest_before_start": include_latest_before_start, + } + ) + return await client.get( + f"/series/{series_ticker}/events/{event_ticker}/candlesticks", + params=params or None, + ) + + async def get_multivariate_events( client: RestClient, *, diff --git a/src/kyro/rest/api/portfolio.py b/src/kyro/rest/api/portfolio.py index ccca6b8..4eb8dbd 100644 --- a/src/kyro/rest/api/portfolio.py +++ b/src/kyro/rest/api/portfolio.py @@ -15,6 +15,15 @@ def _clean(params: dict[str, Any]) -> dict[str, Any]: return {k: v for k, v in params.items() if v is not None} +async def get_portfolio(client: RestClient) -> Any: + """Get portfolio summary. `GET /portfolio`. + + Auth required. May not be available for all accounts; prefer get_balance, + get_positions, get_fills for specific data. + """ + return await client.get("/portfolio") + + async def get_balance(client: RestClient) -> Any: """Get balance and portfolio value. `GET /portfolio/balance`. diff --git a/src/kyro/rest/api/search.py b/src/kyro/rest/api/search.py new file mode 100644 index 0000000..69a68a0 --- /dev/null +++ b/src/kyro/rest/api/search.py @@ -0,0 +1,27 @@ +"""Search endpoints. + +Ref: https://docs.kalshi.com (search / filters) +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from kyro.rest.client import RestClient + + +async def get_sports_filters(client: RestClient) -> Any: + """Get filters by sport. `GET /search/filters_by_sport`. + + Returns sport-based filter metadata for search/discovery. + """ + return await client.get("/search/filters_by_sport") + + +async def get_tags_by_categories(client: RestClient) -> Any: + """Get tags grouped by category. `GET /search/tags_by_categories`. + + Returns tag metadata for search/filtering. + """ + return await client.get("/search/tags_by_categories") From de96afe861f00c20719cba52cfbc7070358550d4 Mon Sep 17 00:00:00 2001 From: Brian Hartford Date: Sat, 24 Jan 2026 19:32:18 -0500 Subject: [PATCH 3/3] Linting --- tests/conftest.py | 47 +++++++++++++++++++++++++++++++++ tests/test_api_modules.py | 55 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f571fc4..53b646f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,24 +100,71 @@ async def _portfolio_order_delete(r: web.Request) -> web.Response: ) +async def _portfolio_order_decrease(r: web.Request) -> web.Response: + return _mk_json( + { + "order": {"order_id": r.match_info["order_id"]}, + "reduced_by": 1, + "reduced_by_fp": "1.00", + } + ) + + +async def _event_candlesticks(r: web.Request) -> web.Response: + return _mk_json({"candlesticks": []}) + + +async def _search_filters_by_sport(_: web.Request) -> web.Response: + return _mk_json({"sports": []}) + + +async def _search_tags_by_categories(_: web.Request) -> web.Response: + return _mk_json({"categories": []}) + + +async def _portfolio_summary(_: web.Request) -> web.Response: + return _mk_json({"portfolio_value": 10000, "balance": 10000}) + + +async def _live_data(r: web.Request) -> web.Response: + if "tickers" in r.query: + return _mk_json({"tickers": r.query.get("tickers", "").split(",")}) + return _mk_json({"ticker": r.query.get("ticker", "")}) + + +async def _exchange_user_data_timestamp(_: web.Request) -> web.Response: + return _mk_json({"timestamp": 1704067200}) + + def create_kalshi_app() -> web.Application: """Minimal aiohttp app that mimics Kalshi-style routes for testing.""" app = web.Application() # Exchange app.router.add_get("/exchange/status", _exchange_status) + app.router.add_get("/exchange/user-data-timestamp", _exchange_user_data_timestamp) # Markets app.router.add_get("/markets", _markets_list) app.router.add_get(r"/markets/{ticker}", _market_detail) app.router.add_get(r"/markets/{ticker}/orderbook", _market_orderbook) app.router.add_get("/markets/trades", _markets_trades) + app.router.add_get("/live-data", _live_data) # Events app.router.add_get("/events", _events_list) app.router.add_get(r"/events/{ticker}", _event_detail) + app.router.add_get( + r"/series/{series_ticker}/events/{event_ticker}/candlesticks", + _event_candlesticks, + ) + # Search + app.router.add_get("/search/filters_by_sport", _search_filters_by_sport) + app.router.add_get("/search/tags_by_categories", _search_tags_by_categories) # Portfolio (auth-style; we don't enforce auth in tests) + app.router.add_get("/portfolio", _portfolio_summary) app.router.add_get("/portfolio/balance", _portfolio_balance) app.router.add_get("/portfolio/orders", _portfolio_orders_list) app.router.add_get(r"/portfolio/orders/{order_id}", _portfolio_order_detail) app.router.add_post("/portfolio/orders", _portfolio_order_create) + app.router.add_post(r"/portfolio/orders/{order_id}/decrease", _portfolio_order_decrease) app.router.add_delete(r"/portfolio/orders/{order_id}", _portfolio_order_delete) # Test helpers: empty, errors, echo, params, slow app.router.add_get("/empty", _empty) diff --git a/tests/test_api_modules.py b/tests/test_api_modules.py index d43a360..df7bbba 100644 --- a/tests/test_api_modules.py +++ b/tests/test_api_modules.py @@ -1,9 +1,9 @@ -"""Tests for rest.api modules (exchange, markets, events, orders, portfolio).""" +"""Tests for rest.api modules (exchange, markets, events, search, orders, portfolio).""" from __future__ import annotations from kyro import RestClient -from kyro.rest.api import events, exchange, markets, orders, portfolio +from kyro.rest.api import events, exchange, markets, orders, portfolio, search async def test_get_exchange_status(kyro_client: RestClient) -> None: @@ -12,6 +12,12 @@ async def test_get_exchange_status(kyro_client: RestClient) -> None: assert "trading_active" in data +async def test_get_user_data_timestamp(kyro_client: RestClient) -> None: + data = await exchange.get_user_data_timestamp(kyro_client) + assert "timestamp" in data + assert isinstance(data["timestamp"], int) + + async def test_get_markets(kyro_client: RestClient) -> None: data = await markets.get_markets(kyro_client) assert "markets" in data @@ -43,6 +49,18 @@ async def test_get_trades(kyro_client: RestClient) -> None: assert "cursor" in data +async def test_get_live_data(kyro_client: RestClient) -> None: + data = await markets.get_live_data(kyro_client, "KXBTC-24JAN15") + assert "ticker" in data + assert data["ticker"] == "KXBTC-24JAN15" + + +async def test_get_multiple_live_data(kyro_client: RestClient) -> None: + data = await markets.get_multiple_live_data(kyro_client, "KXBTC-24JAN15,INXD-25") + assert "tickers" in data + assert isinstance(data["tickers"], list) + + async def test_get_events(kyro_client: RestClient) -> None: data = await events.get_events(kyro_client) assert "events" in data @@ -56,6 +74,25 @@ async def test_get_event(kyro_client: RestClient) -> None: assert "markets" in data +async def test_get_event_candlesticks(kyro_client: RestClient) -> None: + data = await events.get_event_candlesticks( + kyro_client, "KXBTC", "INXD-25", period_interval=60, limit=100 + ) + assert "candlesticks" in data + assert isinstance(data["candlesticks"], list) + + +async def test_get_sports_filters(kyro_client: RestClient) -> None: + data = await search.get_sports_filters(kyro_client) + assert "sports" in data + assert isinstance(data["sports"], list) + + +async def test_get_tags_by_categories(kyro_client: RestClient) -> None: + data = await search.get_tags_by_categories(kyro_client) + assert "categories" in data + + async def test_get_orders(kyro_client: RestClient) -> None: data = await orders.get_orders(kyro_client) assert "orders" in data @@ -86,6 +123,20 @@ async def test_cancel_order(kyro_client: RestClient) -> None: assert "order" in data or "reduced_by" in data +async def test_decrease_order(kyro_client: RestClient) -> None: + data = await orders.decrease_order(kyro_client, "ord-123", reduce_by=1) + assert data is not None + assert "order" in data + assert data["order"]["order_id"] == "ord-123" + assert "reduced_by" in data + + +async def test_get_portfolio(kyro_client: RestClient) -> None: + data = await portfolio.get_portfolio(kyro_client) + assert "portfolio_value" in data + assert "balance" in data + + async def test_get_balance(kyro_client: RestClient) -> None: data = await portfolio.get_balance(kyro_client) assert "balance" in data