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 1be14f0..93de870 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. @@ -16,6 +19,7 @@ API areas are grouped into: - `exchange` - `markets` - `events` +- `search` - `orders` - `portfolio` @@ -31,7 +35,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 @@ -171,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. @@ -186,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( @@ -222,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 @@ -235,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 @@ -255,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 @@ -283,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)** --- @@ -410,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/assets/logo-light-bg.png b/assets/logo-light-bg.png new file mode 100644 index 0000000..7c2c920 Binary files /dev/null and b/assets/logo-light-bg.png differ 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") 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