kalshieye 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kalshieye-0.2.1/LICENSE +21 -0
- kalshieye-0.2.1/PKG-INFO +91 -0
- kalshieye-0.2.1/README.md +68 -0
- kalshieye-0.2.1/kalshieye/__init__.py +7 -0
- kalshieye-0.2.1/kalshieye/book.py +97 -0
- kalshieye-0.2.1/kalshieye/client.py +406 -0
- kalshieye-0.2.1/kalshieye.egg-info/PKG-INFO +91 -0
- kalshieye-0.2.1/kalshieye.egg-info/SOURCES.txt +12 -0
- kalshieye-0.2.1/kalshieye.egg-info/dependency_links.txt +1 -0
- kalshieye-0.2.1/kalshieye.egg-info/requires.txt +7 -0
- kalshieye-0.2.1/kalshieye.egg-info/top_level.txt +1 -0
- kalshieye-0.2.1/pyproject.toml +31 -0
- kalshieye-0.2.1/setup.cfg +4 -0
- kalshieye-0.2.1/tests/test_book.py +50 -0
kalshieye-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kalshi Eye
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kalshieye-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kalshieye
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Download Kalshi Eye flat files (order book, book snapshots, top of book, bars, trades, spot, markets, settlements) and load them into pandas or Polars.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://kalshieye.com
|
|
7
|
+
Project-URL: Documentation, https://kalshieye.com/docs/bulk/
|
|
8
|
+
Project-URL: Source, https://github.com/KalshiEye/kalshieye-python
|
|
9
|
+
Keywords: kalshi,prediction markets,order book,market data,parquet
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: pandas
|
|
18
|
+
Requires-Dist: pandas>=2.0; extra == "pandas"
|
|
19
|
+
Requires-Dist: pyarrow>=14; extra == "pandas"
|
|
20
|
+
Provides-Extra: polars
|
|
21
|
+
Requires-Dist: polars>=0.20; extra == "polars"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# kalshieye
|
|
25
|
+
|
|
26
|
+
Python client for Kalshi Eye flat files: daily Parquet for the order book (`book_l2`), 1-second top-10 book snapshots (`book_snap_1s`), top of book (`bbo`), one-minute bars (`bbo_1m`), trades, crypto spot (`spot`), and the reference tables (`markets`, `settlements`, `series`, `incidents`).
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install "kalshieye[pandas]" # or kalshieye[polars], or plain kalshieye
|
|
30
|
+
export KALSHIEYE_API_KEY=clab_... # create one under API keys
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from kalshieye import Client, replay_book
|
|
35
|
+
|
|
36
|
+
kx = Client()
|
|
37
|
+
kx.datasets() # schemas, coverage, required plan
|
|
38
|
+
kx.estimate(["bbo_1m", "trades"], "2026-10-12", "2026-10-18", series=["KXBTC*"])
|
|
39
|
+
|
|
40
|
+
kx.download(["bbo_1m", "trades"], "2026-10-12", "2026-10-18",
|
|
41
|
+
series=["KXBTC*"], dest="data", progress=True)
|
|
42
|
+
|
|
43
|
+
df = kx.load("bbo_1m", "2026-10-12", "2026-10-18", series="KXBTC15M")
|
|
44
|
+
fills = kx.load("trades", "2026-10-12", "2026-10-12", tickers=["KXBTC15M-26OCT121545-45"])
|
|
45
|
+
|
|
46
|
+
# Rebuild the book from book_l2 rows
|
|
47
|
+
book = kx.load("book_l2", "2026-10-12", "2026-10-12", tickers=["KXBTC15M-26OCT121545-45"])
|
|
48
|
+
for ts, ticker, bids, asks in replay_book(book):
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
# One event in one zip: book, snapshots, trades, spot, metadata.json, settlement.json
|
|
52
|
+
kx.bundle("KXBTC15M-26OCT121545")
|
|
53
|
+
|
|
54
|
+
# CSV.gz copy of a single file
|
|
55
|
+
f = next(kx.files("trades", "2026-10-12", "2026-10-12", series="KXBTC15M"))
|
|
56
|
+
kx.csv(f["key"])
|
|
57
|
+
|
|
58
|
+
# S3 access (paid plans): a read-only key for aws s3 sync, rclone, DuckDB, boto3
|
|
59
|
+
key = kx.create_s3_key("laptop")
|
|
60
|
+
print(key["snippets"]["aws_cli"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Files land under `dest` in the lake layout (`bbo_1m/date=2026-10-12/series=KXBTC15M/...parquet`), so DuckDB, Polars, and Spark can read the folder as a hive-partitioned dataset. `spot` is partitioned by `asset=` instead of `series=`.
|
|
64
|
+
- Every file is checked against its sha256. Files already on disk are skipped and do not count toward your monthly download allowance.
|
|
65
|
+
- `series` accepts wildcards (`KXBTC*`). `categories` filters by archive category (`crypto`, `politics`, ...). `tickers` picks the series files holding those markets, and `load` keeps only their rows.
|
|
66
|
+
- `bundle`, `csv`, and `replay` are built on the server the first time; the client waits for them.
|
|
67
|
+
- Errors raise `kalshieye.ApiError`. Use `err.code` to tell plan limits (`plan_required`, `history_limited`, `download_quota`) apart.
|
|
68
|
+
|
|
69
|
+
## Without an account
|
|
70
|
+
|
|
71
|
+
`replay_book` works on any `book_l2` rows, including the free one-day sample on Hugging Face ([KalshiEye/kalshi-order-book-sample](https://huggingface.co/datasets/KalshiEye/kalshi-order-book-sample)), which DuckDB reads in place:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# pip install "kalshieye[pandas]" duckdb
|
|
75
|
+
import duckdb
|
|
76
|
+
from kalshieye import replay_book
|
|
77
|
+
|
|
78
|
+
# Bills vs Rams, YES if Buffalo wins: every book change up to 11 PM Eastern
|
|
79
|
+
book = duckdb.sql("""
|
|
80
|
+
SELECT * FROM 'hf://datasets/KalshiEye/kalshi-order-book-sample/kalshieye-data/book_l2/*/series=KXNFLGAME/*.parquet'
|
|
81
|
+
WHERE ticker = 'KXNFLGAME-26OCT12BUFLAR-BUF'
|
|
82
|
+
AND ts_recv::TIMESTAMPTZ <= '2026-10-13 03:00:00+00'::TIMESTAMPTZ
|
|
83
|
+
ORDER BY ts_recv, seq
|
|
84
|
+
""").df()
|
|
85
|
+
|
|
86
|
+
*_, (ts, ticker, bids, asks) = replay_book(book, levels=3)
|
|
87
|
+
print(bids) # [(0.51, 155253.51), (0.5, 405328.0), (0.49, 15126.0)]
|
|
88
|
+
print(asks) # [(0.52, 960851.07), (0.53, 862817.41), (0.54, 113351.03)]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
More runnable examples (a backtest, spreads and depth, calibration) are in [KalshiEye/kalshieye-examples](https://github.com/KalshiEye/kalshieye-examples). Every column is described in the [data dictionary](https://kalshieye.com/docs/datasets/).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# kalshieye
|
|
2
|
+
|
|
3
|
+
Python client for Kalshi Eye flat files: daily Parquet for the order book (`book_l2`), 1-second top-10 book snapshots (`book_snap_1s`), top of book (`bbo`), one-minute bars (`bbo_1m`), trades, crypto spot (`spot`), and the reference tables (`markets`, `settlements`, `series`, `incidents`).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install "kalshieye[pandas]" # or kalshieye[polars], or plain kalshieye
|
|
7
|
+
export KALSHIEYE_API_KEY=clab_... # create one under API keys
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from kalshieye import Client, replay_book
|
|
12
|
+
|
|
13
|
+
kx = Client()
|
|
14
|
+
kx.datasets() # schemas, coverage, required plan
|
|
15
|
+
kx.estimate(["bbo_1m", "trades"], "2026-10-12", "2026-10-18", series=["KXBTC*"])
|
|
16
|
+
|
|
17
|
+
kx.download(["bbo_1m", "trades"], "2026-10-12", "2026-10-18",
|
|
18
|
+
series=["KXBTC*"], dest="data", progress=True)
|
|
19
|
+
|
|
20
|
+
df = kx.load("bbo_1m", "2026-10-12", "2026-10-18", series="KXBTC15M")
|
|
21
|
+
fills = kx.load("trades", "2026-10-12", "2026-10-12", tickers=["KXBTC15M-26OCT121545-45"])
|
|
22
|
+
|
|
23
|
+
# Rebuild the book from book_l2 rows
|
|
24
|
+
book = kx.load("book_l2", "2026-10-12", "2026-10-12", tickers=["KXBTC15M-26OCT121545-45"])
|
|
25
|
+
for ts, ticker, bids, asks in replay_book(book):
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
# One event in one zip: book, snapshots, trades, spot, metadata.json, settlement.json
|
|
29
|
+
kx.bundle("KXBTC15M-26OCT121545")
|
|
30
|
+
|
|
31
|
+
# CSV.gz copy of a single file
|
|
32
|
+
f = next(kx.files("trades", "2026-10-12", "2026-10-12", series="KXBTC15M"))
|
|
33
|
+
kx.csv(f["key"])
|
|
34
|
+
|
|
35
|
+
# S3 access (paid plans): a read-only key for aws s3 sync, rclone, DuckDB, boto3
|
|
36
|
+
key = kx.create_s3_key("laptop")
|
|
37
|
+
print(key["snippets"]["aws_cli"])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Files land under `dest` in the lake layout (`bbo_1m/date=2026-10-12/series=KXBTC15M/...parquet`), so DuckDB, Polars, and Spark can read the folder as a hive-partitioned dataset. `spot` is partitioned by `asset=` instead of `series=`.
|
|
41
|
+
- Every file is checked against its sha256. Files already on disk are skipped and do not count toward your monthly download allowance.
|
|
42
|
+
- `series` accepts wildcards (`KXBTC*`). `categories` filters by archive category (`crypto`, `politics`, ...). `tickers` picks the series files holding those markets, and `load` keeps only their rows.
|
|
43
|
+
- `bundle`, `csv`, and `replay` are built on the server the first time; the client waits for them.
|
|
44
|
+
- Errors raise `kalshieye.ApiError`. Use `err.code` to tell plan limits (`plan_required`, `history_limited`, `download_quota`) apart.
|
|
45
|
+
|
|
46
|
+
## Without an account
|
|
47
|
+
|
|
48
|
+
`replay_book` works on any `book_l2` rows, including the free one-day sample on Hugging Face ([KalshiEye/kalshi-order-book-sample](https://huggingface.co/datasets/KalshiEye/kalshi-order-book-sample)), which DuckDB reads in place:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# pip install "kalshieye[pandas]" duckdb
|
|
52
|
+
import duckdb
|
|
53
|
+
from kalshieye import replay_book
|
|
54
|
+
|
|
55
|
+
# Bills vs Rams, YES if Buffalo wins: every book change up to 11 PM Eastern
|
|
56
|
+
book = duckdb.sql("""
|
|
57
|
+
SELECT * FROM 'hf://datasets/KalshiEye/kalshi-order-book-sample/kalshieye-data/book_l2/*/series=KXNFLGAME/*.parquet'
|
|
58
|
+
WHERE ticker = 'KXNFLGAME-26OCT12BUFLAR-BUF'
|
|
59
|
+
AND ts_recv::TIMESTAMPTZ <= '2026-10-13 03:00:00+00'::TIMESTAMPTZ
|
|
60
|
+
ORDER BY ts_recv, seq
|
|
61
|
+
""").df()
|
|
62
|
+
|
|
63
|
+
*_, (ts, ticker, bids, asks) = replay_book(book, levels=3)
|
|
64
|
+
print(bids) # [(0.51, 155253.51), (0.5, 405328.0), (0.49, 15126.0)]
|
|
65
|
+
print(asks) # [(0.52, 960851.07), (0.53, 862817.41), (0.54, 113351.03)]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
More runnable examples (a backtest, spreads and depth, calibration) are in [KalshiEye/kalshieye-examples](https://github.com/KalshiEye/kalshieye-examples). Every column is described in the [data dictionary](https://kalshieye.com/docs/datasets/).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Rebuild Kalshi order books from ``book_l2`` rows.
|
|
2
|
+
|
|
3
|
+
from kalshieye import replay_book
|
|
4
|
+
for ts, ticker, bids, asks in replay_book(df, ticker="KXBTC15M-26OCT121545-45"):
|
|
5
|
+
... # bids/asks: [(price, size), ...] best first, YES terms
|
|
6
|
+
|
|
7
|
+
Kalshi books are bids only. A NO bid at p is a YES ask at 1 - p, so asks here
|
|
8
|
+
are derived from the NO side.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Iterable, Iterator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Book:
|
|
17
|
+
"""One ticker's book. ``apply`` takes book_l2 rows in file order."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.yes: dict[float, float] = {}
|
|
21
|
+
self.no: dict[float, float] = {}
|
|
22
|
+
self._snap_ts: Any = None
|
|
23
|
+
|
|
24
|
+
def apply(self, row: dict[str, Any]) -> None:
|
|
25
|
+
side, price, size = row.get("side"), row.get("price"), row.get("size")
|
|
26
|
+
if row.get("is_snapshot"):
|
|
27
|
+
if row.get("ts_recv") != self._snap_ts:
|
|
28
|
+
self.yes.clear()
|
|
29
|
+
self.no.clear()
|
|
30
|
+
self._snap_ts = row.get("ts_recv")
|
|
31
|
+
if side is None or price is None:
|
|
32
|
+
return
|
|
33
|
+
book = self.yes if side == "yes" else self.no
|
|
34
|
+
if size and size > 0:
|
|
35
|
+
book[float(price)] = float(size)
|
|
36
|
+
else:
|
|
37
|
+
book.pop(float(price), None)
|
|
38
|
+
return
|
|
39
|
+
self._snap_ts = None
|
|
40
|
+
if side is None or price is None or size is None:
|
|
41
|
+
return
|
|
42
|
+
book = self.yes if side == "yes" else self.no
|
|
43
|
+
p = float(price)
|
|
44
|
+
# Sizes are fractional; rounding keeps float residue from leaving a phantom level.
|
|
45
|
+
left = round(book.get(p, 0.0) + float(size), 6)
|
|
46
|
+
if left > 0:
|
|
47
|
+
book[p] = left
|
|
48
|
+
else:
|
|
49
|
+
book.pop(p, None)
|
|
50
|
+
|
|
51
|
+
def bids(self, levels: int = 10) -> list[tuple[float, float]]:
|
|
52
|
+
return sorted(self.yes.items(), reverse=True)[:levels]
|
|
53
|
+
|
|
54
|
+
def asks(self, levels: int = 10) -> list[tuple[float, float]]:
|
|
55
|
+
return [(round(1.0 - p, 6), s) for p, s in sorted(self.no.items(), reverse=True)[:levels]]
|
|
56
|
+
|
|
57
|
+
def best(self) -> tuple[float | None, float | None]:
|
|
58
|
+
b = max(self.yes) if self.yes else None
|
|
59
|
+
a = round(1.0 - max(self.no), 6) if self.no else None
|
|
60
|
+
return b, a
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _rows(data: Any) -> Iterable[dict[str, Any]]:
|
|
64
|
+
if hasattr(data, "to_pylist"):
|
|
65
|
+
return data.to_pylist()
|
|
66
|
+
if hasattr(data, "to_dicts"):
|
|
67
|
+
return data.to_dicts()
|
|
68
|
+
if hasattr(data, "to_dict") and hasattr(data, "columns"):
|
|
69
|
+
return data.to_dict("records")
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def replay_book(
|
|
74
|
+
data: Any, *, ticker: str | None = None, levels: int = 10
|
|
75
|
+
) -> Iterator[tuple[Any, str, list[tuple[float, float]], list[tuple[float, float]]]]:
|
|
76
|
+
"""Yield ``(ts_recv, ticker, bids, asks)`` after each book message.
|
|
77
|
+
|
|
78
|
+
``data`` is a pandas or polars DataFrame, a pyarrow Table, or an iterable of
|
|
79
|
+
dicts with the book_l2 columns. Rows of one message share ts_recv; the state
|
|
80
|
+
is yielded once the message is fully applied.
|
|
81
|
+
"""
|
|
82
|
+
books: dict[str, Book] = {}
|
|
83
|
+
pending: tuple[Any, str] | None = None
|
|
84
|
+
want = ticker.upper() if ticker else None
|
|
85
|
+
for row in _rows(data):
|
|
86
|
+
t = str(row.get("ticker") or "")
|
|
87
|
+
if want and t.upper() != want:
|
|
88
|
+
continue
|
|
89
|
+
key = (row.get("ts_recv"), t)
|
|
90
|
+
if pending is not None and key != pending:
|
|
91
|
+
b = books[pending[1]]
|
|
92
|
+
yield pending[0], pending[1], b.bids(levels), b.asks(levels)
|
|
93
|
+
books.setdefault(t, Book()).apply(row)
|
|
94
|
+
pending = key
|
|
95
|
+
if pending is not None:
|
|
96
|
+
b = books[pending[1]]
|
|
97
|
+
yield pending[0], pending[1], b.bids(levels), b.asks(levels)
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Small, dependency-free client for the Kalshi Eye data API.
|
|
2
|
+
|
|
3
|
+
from kalshieye import Client
|
|
4
|
+
|
|
5
|
+
kx = Client() # reads KALSHIEYE_API_KEY
|
|
6
|
+
kx.estimate(["bbo_1m"], "2026-10-01", "2026-10-07", series=["KXBTC*"])
|
|
7
|
+
paths = kx.download(["bbo_1m"], "2026-10-01", "2026-10-07", series=["KXBTC*"], dest="data")
|
|
8
|
+
df = kx.load("bbo_1m", "2026-10-01", "2026-10-07", series=["KXBTC15M"]) # pandas
|
|
9
|
+
df = kx.load("trades", "2026-10-12", "2026-10-12", tickers=["KXBTC15M-26OCT121545-45"])
|
|
10
|
+
zip_path = kx.bundle("KXBTC15M-26OCT121545") # one event: book, trades, spot, settlement
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import time
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.parse
|
|
22
|
+
import urllib.request
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Iterable, Iterator, Sequence
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = "https://kalshieye.com"
|
|
27
|
+
_LINK_BATCH = 200
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ApiError(RuntimeError):
|
|
31
|
+
def __init__(self, status: int, detail: Any) -> None:
|
|
32
|
+
self.status = status
|
|
33
|
+
self.detail = detail
|
|
34
|
+
if isinstance(detail, dict):
|
|
35
|
+
message = detail.get("message") or json.dumps(detail)
|
|
36
|
+
else:
|
|
37
|
+
message = str(detail)
|
|
38
|
+
super().__init__(f"{status}: {message}")
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def code(self) -> str | None:
|
|
42
|
+
return self.detail.get("code") if isinstance(self.detail, dict) else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _sha256(path: Path) -> str:
|
|
46
|
+
h = hashlib.sha256()
|
|
47
|
+
with path.open("rb") as fh:
|
|
48
|
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
|
49
|
+
h.update(chunk)
|
|
50
|
+
return h.hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _as_list(value: str | Iterable[str] | None) -> list[str]:
|
|
54
|
+
if value is None:
|
|
55
|
+
return []
|
|
56
|
+
if isinstance(value, str):
|
|
57
|
+
return [value]
|
|
58
|
+
return [str(v) for v in value]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Client:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
api_key: str | None = None,
|
|
65
|
+
*,
|
|
66
|
+
base_url: str | None = None,
|
|
67
|
+
timeout: float = 120.0,
|
|
68
|
+
poll_interval: float = 2.0,
|
|
69
|
+
) -> None:
|
|
70
|
+
self.api_key = api_key if api_key is not None else os.environ.get("KALSHIEYE_API_KEY")
|
|
71
|
+
self.base_url = (base_url or os.environ.get("KALSHIEYE_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
72
|
+
self.timeout = timeout
|
|
73
|
+
self.poll_interval = poll_interval
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------ transport
|
|
76
|
+
|
|
77
|
+
def _request(self, method: str, path: str, *, params: dict[str, Any] | None = None, body: Any = None) -> Any:
|
|
78
|
+
url = self.base_url + path
|
|
79
|
+
if params:
|
|
80
|
+
pairs: list[tuple[str, str]] = []
|
|
81
|
+
for k, v in params.items():
|
|
82
|
+
if v is None:
|
|
83
|
+
continue
|
|
84
|
+
for item in v if isinstance(v, (list, tuple)) else [v]:
|
|
85
|
+
pairs.append((k, str(item).lower() if isinstance(item, bool) else str(item)))
|
|
86
|
+
if pairs:
|
|
87
|
+
url += "?" + urllib.parse.urlencode(pairs)
|
|
88
|
+
headers = {"Accept": "application/json", "User-Agent": "kalshieye-python/0.2"}
|
|
89
|
+
if self.api_key:
|
|
90
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
91
|
+
data = None
|
|
92
|
+
if body is not None:
|
|
93
|
+
data = json.dumps(body).encode()
|
|
94
|
+
headers["Content-Type"] = "application/json"
|
|
95
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
96
|
+
try:
|
|
97
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as res:
|
|
98
|
+
raw = res.read()
|
|
99
|
+
except urllib.error.HTTPError as exc:
|
|
100
|
+
text = exc.read().decode("utf-8", errors="replace")
|
|
101
|
+
try:
|
|
102
|
+
detail = json.loads(text).get("detail", text)
|
|
103
|
+
except (json.JSONDecodeError, AttributeError):
|
|
104
|
+
detail = text
|
|
105
|
+
raise ApiError(exc.code, detail) from None
|
|
106
|
+
return json.loads(raw) if raw else None
|
|
107
|
+
|
|
108
|
+
def _fetch(self, url: str, dest: Path) -> None:
|
|
109
|
+
with urllib.request.urlopen(url, timeout=self.timeout) as res, dest.open("wb") as fh:
|
|
110
|
+
shutil.copyfileobj(res, fh, length=1 << 20)
|
|
111
|
+
|
|
112
|
+
def _need_key(self) -> None:
|
|
113
|
+
if not self.api_key:
|
|
114
|
+
raise ApiError(401, "Set KALSHIEYE_API_KEY or pass api_key= (create a key under API keys).")
|
|
115
|
+
|
|
116
|
+
def _poll(self, call: Callable[[], dict[str, Any]], *, wait: float) -> dict[str, Any]:
|
|
117
|
+
"""Repeat ``call`` while the server reports ``building``."""
|
|
118
|
+
deadline = time.monotonic() + wait
|
|
119
|
+
while True:
|
|
120
|
+
out = call()
|
|
121
|
+
status = out.get("status")
|
|
122
|
+
if status == "error":
|
|
123
|
+
raise ApiError(500, out.get("error") or "build failed")
|
|
124
|
+
if status != "building" or time.monotonic() >= deadline:
|
|
125
|
+
return out
|
|
126
|
+
time.sleep(self.poll_interval)
|
|
127
|
+
|
|
128
|
+
def _save(self, f: dict[str, Any], dest: Path) -> Path:
|
|
129
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
tmp = dest.with_name(dest.name + ".part")
|
|
131
|
+
self._fetch(f["url"], tmp)
|
|
132
|
+
tmp.replace(dest)
|
|
133
|
+
return dest
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------ catalog
|
|
136
|
+
|
|
137
|
+
def datasets(self) -> list[dict[str, Any]]:
|
|
138
|
+
"""Every dataset with columns, coverage, required plan, and sample links."""
|
|
139
|
+
return self._request("GET", "/api/v1/datasets")["datasets"]
|
|
140
|
+
|
|
141
|
+
def dataset(self, dataset_id: str) -> dict[str, Any]:
|
|
142
|
+
return self._request("GET", f"/api/v1/datasets/{dataset_id}")
|
|
143
|
+
|
|
144
|
+
def series(self, dataset_id: str, q: str | None = None, *, limit: int = 500) -> list[dict[str, Any]]:
|
|
145
|
+
"""Per-series coverage (days, rows, bytes, first/last date) for a dataset."""
|
|
146
|
+
out: list[dict[str, Any]] = []
|
|
147
|
+
offset = 0
|
|
148
|
+
while True:
|
|
149
|
+
page = self._request(
|
|
150
|
+
"GET",
|
|
151
|
+
f"/api/v1/datasets/{dataset_id}/series",
|
|
152
|
+
params={"q": q, "offset": offset, "limit": min(limit, 500)},
|
|
153
|
+
)
|
|
154
|
+
out.extend(page["series"])
|
|
155
|
+
offset += len(page["series"])
|
|
156
|
+
if not page["series"] or offset >= page["total"]:
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
def plan(self) -> dict[str, Any]:
|
|
160
|
+
self._need_key()
|
|
161
|
+
return self._request("GET", "/api/v1/plan")
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------------ selection
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _selection(
|
|
167
|
+
datasets: str | Sequence[str],
|
|
168
|
+
start_date: str,
|
|
169
|
+
end_date: str,
|
|
170
|
+
series: str | Iterable[str] | None,
|
|
171
|
+
categories: str | Iterable[str] | None,
|
|
172
|
+
tickers: str | Iterable[str] | None = None,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
return {
|
|
175
|
+
"datasets": _as_list(datasets),
|
|
176
|
+
"start_date": str(start_date),
|
|
177
|
+
"end_date": str(end_date),
|
|
178
|
+
"series": _as_list(series),
|
|
179
|
+
"categories": _as_list(categories),
|
|
180
|
+
"tickers": [t.upper() for t in _as_list(tickers)],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def estimate(
|
|
184
|
+
self,
|
|
185
|
+
datasets: str | Sequence[str],
|
|
186
|
+
start_date: str,
|
|
187
|
+
end_date: str,
|
|
188
|
+
*,
|
|
189
|
+
series: str | Iterable[str] | None = None,
|
|
190
|
+
categories: str | Iterable[str] | None = None,
|
|
191
|
+
tickers: str | Iterable[str] | None = None,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""Files, rows, and bytes for a selection, plus what your plan and allowance cover."""
|
|
194
|
+
return self._request(
|
|
195
|
+
"POST",
|
|
196
|
+
"/api/v1/requests/estimate",
|
|
197
|
+
body=self._selection(datasets, start_date, end_date, series, categories, tickers),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def files(
|
|
201
|
+
self,
|
|
202
|
+
datasets: str | Sequence[str],
|
|
203
|
+
start_date: str,
|
|
204
|
+
end_date: str,
|
|
205
|
+
*,
|
|
206
|
+
series: str | Iterable[str] | None = None,
|
|
207
|
+
categories: str | Iterable[str] | None = None,
|
|
208
|
+
tickers: str | Iterable[str] | None = None,
|
|
209
|
+
page_size: int = 500,
|
|
210
|
+
) -> Iterator[dict[str, Any]]:
|
|
211
|
+
"""Yield file entries (dataset, date, series, key, path, rows, bytes, sha256). No links, no quota.
|
|
212
|
+
|
|
213
|
+
``tickers`` selects the series files that hold those markets; the files
|
|
214
|
+
themselves hold the whole series.
|
|
215
|
+
"""
|
|
216
|
+
self._need_key()
|
|
217
|
+
sel = self._selection(datasets, start_date, end_date, series, categories, tickers)
|
|
218
|
+
cursor: str | None = None
|
|
219
|
+
while True:
|
|
220
|
+
page = self._request(
|
|
221
|
+
"GET",
|
|
222
|
+
"/api/v1/files",
|
|
223
|
+
params={
|
|
224
|
+
"dataset": sel["datasets"],
|
|
225
|
+
"start_date": sel["start_date"],
|
|
226
|
+
"end_date": sel["end_date"],
|
|
227
|
+
"series": sel["series"],
|
|
228
|
+
"category": sel["categories"],
|
|
229
|
+
"ticker": sel["tickers"],
|
|
230
|
+
"cursor": cursor,
|
|
231
|
+
"limit": page_size,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
yield from page["files"]
|
|
235
|
+
cursor = page.get("next_cursor")
|
|
236
|
+
if not cursor:
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
def links(self, keys: Sequence[str]) -> list[dict[str, Any]]:
|
|
240
|
+
"""Signed links (valid one hour) for file keys. New files count toward your monthly GB."""
|
|
241
|
+
self._need_key()
|
|
242
|
+
out: list[dict[str, Any]] = []
|
|
243
|
+
for i in range(0, len(keys), _LINK_BATCH):
|
|
244
|
+
out.extend(self._request("POST", "/api/v1/files/links", body={"keys": list(keys[i : i + _LINK_BATCH])})["files"])
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
def download(
|
|
248
|
+
self,
|
|
249
|
+
datasets: str | Sequence[str],
|
|
250
|
+
start_date: str,
|
|
251
|
+
end_date: str,
|
|
252
|
+
*,
|
|
253
|
+
series: str | Iterable[str] | None = None,
|
|
254
|
+
categories: str | Iterable[str] | None = None,
|
|
255
|
+
tickers: str | Iterable[str] | None = None,
|
|
256
|
+
dest: str | Path = "kalshieye-data",
|
|
257
|
+
progress: bool = False,
|
|
258
|
+
) -> list[Path]:
|
|
259
|
+
"""Download a selection into ``dest`` (lake layout), verify sha256, skip files already there."""
|
|
260
|
+
root = Path(dest)
|
|
261
|
+
entries = list(
|
|
262
|
+
self.files(datasets, start_date, end_date, series=series, categories=categories, tickers=tickers)
|
|
263
|
+
)
|
|
264
|
+
paths: list[Path] = []
|
|
265
|
+
need: list[dict[str, Any]] = []
|
|
266
|
+
for f in entries:
|
|
267
|
+
p = root / f["path"]
|
|
268
|
+
paths.append(p)
|
|
269
|
+
if not (p.is_file() and (not f.get("sha256") or _sha256(p) == f["sha256"])):
|
|
270
|
+
need.append(f)
|
|
271
|
+
for f in self.links([f["key"] for f in need]):
|
|
272
|
+
p = root / f["path"]
|
|
273
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
tmp = p.with_name(p.name + ".part")
|
|
275
|
+
self._fetch(f["url"], tmp)
|
|
276
|
+
if f.get("sha256") and _sha256(tmp) != f["sha256"]:
|
|
277
|
+
tmp.unlink()
|
|
278
|
+
raise ApiError(0, f"checksum mismatch: {f['path']}")
|
|
279
|
+
tmp.replace(p)
|
|
280
|
+
if progress:
|
|
281
|
+
print(f"{f['path']} {f['bytes'] / 1e6:.1f} MB")
|
|
282
|
+
return paths
|
|
283
|
+
|
|
284
|
+
def load(
|
|
285
|
+
self,
|
|
286
|
+
dataset: str,
|
|
287
|
+
start_date: str,
|
|
288
|
+
end_date: str,
|
|
289
|
+
*,
|
|
290
|
+
series: str | Iterable[str] | None = None,
|
|
291
|
+
categories: str | Iterable[str] | None = None,
|
|
292
|
+
tickers: str | Iterable[str] | None = None,
|
|
293
|
+
dest: str | Path = "kalshieye-data",
|
|
294
|
+
engine: str = "pandas",
|
|
295
|
+
):
|
|
296
|
+
"""Download (if needed) and load one dataset: engine is "pandas", "polars", or "arrow".
|
|
297
|
+
|
|
298
|
+
With ``tickers``, only those markets' rows are kept.
|
|
299
|
+
"""
|
|
300
|
+
if engine not in ("pandas", "polars", "arrow"):
|
|
301
|
+
raise ValueError("engine must be pandas, polars, or arrow")
|
|
302
|
+
want = sorted({t.upper() for t in _as_list(tickers)})
|
|
303
|
+
paths = self.download(
|
|
304
|
+
dataset, start_date, end_date, series=series, categories=categories, tickers=want or None, dest=dest
|
|
305
|
+
)
|
|
306
|
+
import pyarrow as pa
|
|
307
|
+
import pyarrow.compute as pc
|
|
308
|
+
import pyarrow.parquet as pq
|
|
309
|
+
|
|
310
|
+
tables = []
|
|
311
|
+
for p in paths:
|
|
312
|
+
t = pq.read_table(p)
|
|
313
|
+
if want and "ticker" in t.column_names:
|
|
314
|
+
col = t.column("ticker")
|
|
315
|
+
if pa.types.is_dictionary(col.type):
|
|
316
|
+
col = col.cast(pa.string())
|
|
317
|
+
t = t.filter(pc.is_in(col, value_set=pa.array(want)))
|
|
318
|
+
tables.append(t)
|
|
319
|
+
table = pa.concat_tables(tables, promote_options="default") if tables else pa.table({})
|
|
320
|
+
if engine == "polars":
|
|
321
|
+
import polars as pl
|
|
322
|
+
|
|
323
|
+
return pl.from_arrow(table) if tables else pl.DataFrame()
|
|
324
|
+
return table if engine == "arrow" else table.to_pandas()
|
|
325
|
+
|
|
326
|
+
# ------------------------------------------------------------------ derived files
|
|
327
|
+
|
|
328
|
+
def csv(
|
|
329
|
+
self,
|
|
330
|
+
key: str,
|
|
331
|
+
*,
|
|
332
|
+
tickers: str | Iterable[str] | None = None,
|
|
333
|
+
dest: str | Path = "kalshieye-data",
|
|
334
|
+
wait: float = 600.0,
|
|
335
|
+
) -> Path:
|
|
336
|
+
"""A gzipped CSV copy of one file from ``files()``, optionally cut to ``tickers``."""
|
|
337
|
+
self._need_key()
|
|
338
|
+
body = {"key": key, "tickers": [t.upper() for t in _as_list(tickers)]}
|
|
339
|
+
out = self._poll(lambda: self._request("POST", "/api/v1/files/csv", body=body), wait=wait)
|
|
340
|
+
if out.get("status") != "ready":
|
|
341
|
+
raise ApiError(504, "CSV is still being built; call again later.")
|
|
342
|
+
f = out["file"]
|
|
343
|
+
rel = f["path"][: -len(".parquet")] if f["path"].endswith(".parquet") else f["path"]
|
|
344
|
+
suffix = ".tickers.csv.gz" if body["tickers"] else ".csv.gz"
|
|
345
|
+
return self._save(f, Path(dest) / (rel + suffix))
|
|
346
|
+
|
|
347
|
+
def bundle(self, event: str, *, dest: str | Path = "kalshieye-bundles", wait: float = 900.0) -> Path:
|
|
348
|
+
"""Zip of one Kalshi event: book_l2, book_snap_1s, bbo, trades, spot (what your
|
|
349
|
+
plan includes), plus metadata.json and settlement.json."""
|
|
350
|
+
self._need_key()
|
|
351
|
+
eid = event.strip().upper()
|
|
352
|
+
out = self._poll(lambda: self._request("POST", f"/api/v1/events/{urllib.parse.quote(eid)}/bundle"), wait=wait)
|
|
353
|
+
if out.get("status") != "ready":
|
|
354
|
+
raise ApiError(504, "Bundle is still being built; call again later.")
|
|
355
|
+
return self._save(out["file"], Path(dest) / out["file"]["filename"])
|
|
356
|
+
|
|
357
|
+
def replay(self, ticker: str, *, date: str | None = None, wait: float = 300.0) -> dict[str, Any]:
|
|
358
|
+
"""Top-10 book frames (1s, thinned to at most 3600) and trades for one market-day."""
|
|
359
|
+
self._need_key()
|
|
360
|
+
params = {"date": date} if date else None
|
|
361
|
+
path = f"/api/v1/markets/{urllib.parse.quote(ticker.strip().upper())}/replay"
|
|
362
|
+
out = self._poll(lambda: self._request("GET", path, params=params), wait=wait)
|
|
363
|
+
if out.get("status") != "ready":
|
|
364
|
+
raise ApiError(504, "Replay is still being prepared; call again later.")
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
def create_s3_key(self, name: str = "default") -> dict[str, Any]:
|
|
368
|
+
"""Create a read-only S3 key for syncing whole datasets (paid plans).
|
|
369
|
+
|
|
370
|
+
Returns endpoint, bucket, access_key_id, secret_access_key (shown once), and
|
|
371
|
+
ready-to-paste snippets for aws, rclone, DuckDB, and boto3.
|
|
372
|
+
"""
|
|
373
|
+
self._need_key()
|
|
374
|
+
return self._request("POST", "/api/v1/files/keys", body={"name": name})
|
|
375
|
+
|
|
376
|
+
def s3_keys(self) -> dict[str, Any]:
|
|
377
|
+
"""Your S3 keys, with last use and data counted per key."""
|
|
378
|
+
self._need_key()
|
|
379
|
+
return self._request("GET", "/api/v1/files/keys")
|
|
380
|
+
|
|
381
|
+
def revoke_s3_key(self, key_id: str) -> dict[str, Any]:
|
|
382
|
+
self._need_key()
|
|
383
|
+
return self._request("DELETE", f"/api/v1/files/keys/{urllib.parse.quote(key_id)}")
|
|
384
|
+
|
|
385
|
+
# ------------------------------------------------------------------ saved requests
|
|
386
|
+
|
|
387
|
+
def save_request(
|
|
388
|
+
self,
|
|
389
|
+
datasets: str | Sequence[str],
|
|
390
|
+
start_date: str,
|
|
391
|
+
end_date: str,
|
|
392
|
+
*,
|
|
393
|
+
series: str | Iterable[str] | None = None,
|
|
394
|
+
categories: str | Iterable[str] | None = None,
|
|
395
|
+
tickers: str | Iterable[str] | None = None,
|
|
396
|
+
name: str | None = None,
|
|
397
|
+
) -> dict[str, Any]:
|
|
398
|
+
self._need_key()
|
|
399
|
+
body = self._selection(datasets, start_date, end_date, series, categories, tickers)
|
|
400
|
+
if name:
|
|
401
|
+
body["name"] = name
|
|
402
|
+
return self._request("POST", "/api/v1/requests", body=body)
|
|
403
|
+
|
|
404
|
+
def requests(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
405
|
+
self._need_key()
|
|
406
|
+
return self._request("GET", "/api/v1/requests", params={"limit": limit})["requests"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kalshieye
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Download Kalshi Eye flat files (order book, book snapshots, top of book, bars, trades, spot, markets, settlements) and load them into pandas or Polars.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://kalshieye.com
|
|
7
|
+
Project-URL: Documentation, https://kalshieye.com/docs/bulk/
|
|
8
|
+
Project-URL: Source, https://github.com/KalshiEye/kalshieye-python
|
|
9
|
+
Keywords: kalshi,prediction markets,order book,market data,parquet
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: pandas
|
|
18
|
+
Requires-Dist: pandas>=2.0; extra == "pandas"
|
|
19
|
+
Requires-Dist: pyarrow>=14; extra == "pandas"
|
|
20
|
+
Provides-Extra: polars
|
|
21
|
+
Requires-Dist: polars>=0.20; extra == "polars"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# kalshieye
|
|
25
|
+
|
|
26
|
+
Python client for Kalshi Eye flat files: daily Parquet for the order book (`book_l2`), 1-second top-10 book snapshots (`book_snap_1s`), top of book (`bbo`), one-minute bars (`bbo_1m`), trades, crypto spot (`spot`), and the reference tables (`markets`, `settlements`, `series`, `incidents`).
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install "kalshieye[pandas]" # or kalshieye[polars], or plain kalshieye
|
|
30
|
+
export KALSHIEYE_API_KEY=clab_... # create one under API keys
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from kalshieye import Client, replay_book
|
|
35
|
+
|
|
36
|
+
kx = Client()
|
|
37
|
+
kx.datasets() # schemas, coverage, required plan
|
|
38
|
+
kx.estimate(["bbo_1m", "trades"], "2026-10-12", "2026-10-18", series=["KXBTC*"])
|
|
39
|
+
|
|
40
|
+
kx.download(["bbo_1m", "trades"], "2026-10-12", "2026-10-18",
|
|
41
|
+
series=["KXBTC*"], dest="data", progress=True)
|
|
42
|
+
|
|
43
|
+
df = kx.load("bbo_1m", "2026-10-12", "2026-10-18", series="KXBTC15M")
|
|
44
|
+
fills = kx.load("trades", "2026-10-12", "2026-10-12", tickers=["KXBTC15M-26OCT121545-45"])
|
|
45
|
+
|
|
46
|
+
# Rebuild the book from book_l2 rows
|
|
47
|
+
book = kx.load("book_l2", "2026-10-12", "2026-10-12", tickers=["KXBTC15M-26OCT121545-45"])
|
|
48
|
+
for ts, ticker, bids, asks in replay_book(book):
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
# One event in one zip: book, snapshots, trades, spot, metadata.json, settlement.json
|
|
52
|
+
kx.bundle("KXBTC15M-26OCT121545")
|
|
53
|
+
|
|
54
|
+
# CSV.gz copy of a single file
|
|
55
|
+
f = next(kx.files("trades", "2026-10-12", "2026-10-12", series="KXBTC15M"))
|
|
56
|
+
kx.csv(f["key"])
|
|
57
|
+
|
|
58
|
+
# S3 access (paid plans): a read-only key for aws s3 sync, rclone, DuckDB, boto3
|
|
59
|
+
key = kx.create_s3_key("laptop")
|
|
60
|
+
print(key["snippets"]["aws_cli"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Files land under `dest` in the lake layout (`bbo_1m/date=2026-10-12/series=KXBTC15M/...parquet`), so DuckDB, Polars, and Spark can read the folder as a hive-partitioned dataset. `spot` is partitioned by `asset=` instead of `series=`.
|
|
64
|
+
- Every file is checked against its sha256. Files already on disk are skipped and do not count toward your monthly download allowance.
|
|
65
|
+
- `series` accepts wildcards (`KXBTC*`). `categories` filters by archive category (`crypto`, `politics`, ...). `tickers` picks the series files holding those markets, and `load` keeps only their rows.
|
|
66
|
+
- `bundle`, `csv`, and `replay` are built on the server the first time; the client waits for them.
|
|
67
|
+
- Errors raise `kalshieye.ApiError`. Use `err.code` to tell plan limits (`plan_required`, `history_limited`, `download_quota`) apart.
|
|
68
|
+
|
|
69
|
+
## Without an account
|
|
70
|
+
|
|
71
|
+
`replay_book` works on any `book_l2` rows, including the free one-day sample on Hugging Face ([KalshiEye/kalshi-order-book-sample](https://huggingface.co/datasets/KalshiEye/kalshi-order-book-sample)), which DuckDB reads in place:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# pip install "kalshieye[pandas]" duckdb
|
|
75
|
+
import duckdb
|
|
76
|
+
from kalshieye import replay_book
|
|
77
|
+
|
|
78
|
+
# Bills vs Rams, YES if Buffalo wins: every book change up to 11 PM Eastern
|
|
79
|
+
book = duckdb.sql("""
|
|
80
|
+
SELECT * FROM 'hf://datasets/KalshiEye/kalshi-order-book-sample/kalshieye-data/book_l2/*/series=KXNFLGAME/*.parquet'
|
|
81
|
+
WHERE ticker = 'KXNFLGAME-26OCT12BUFLAR-BUF'
|
|
82
|
+
AND ts_recv::TIMESTAMPTZ <= '2026-10-13 03:00:00+00'::TIMESTAMPTZ
|
|
83
|
+
ORDER BY ts_recv, seq
|
|
84
|
+
""").df()
|
|
85
|
+
|
|
86
|
+
*_, (ts, ticker, bids, asks) = replay_book(book, levels=3)
|
|
87
|
+
print(bids) # [(0.51, 155253.51), (0.5, 405328.0), (0.49, 15126.0)]
|
|
88
|
+
print(asks) # [(0.52, 960851.07), (0.53, 862817.41), (0.54, 113351.03)]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
More runnable examples (a backtest, spreads and depth, calibration) are in [KalshiEye/kalshieye-examples](https://github.com/KalshiEye/kalshieye-examples). Every column is described in the [data dictionary](https://kalshieye.com/docs/datasets/).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
kalshieye/__init__.py
|
|
5
|
+
kalshieye/book.py
|
|
6
|
+
kalshieye/client.py
|
|
7
|
+
kalshieye.egg-info/PKG-INFO
|
|
8
|
+
kalshieye.egg-info/SOURCES.txt
|
|
9
|
+
kalshieye.egg-info/dependency_links.txt
|
|
10
|
+
kalshieye.egg-info/requires.txt
|
|
11
|
+
kalshieye.egg-info/top_level.txt
|
|
12
|
+
tests/test_book.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kalshieye
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kalshieye"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Download Kalshi Eye flat files (order book, book snapshots, top of book, bars, trades, spot, markets, settlements) and load them into pandas or Polars."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["kalshi", "prediction markets", "order book", "market data", "parquet"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
pandas = ["pandas>=2.0", "pyarrow>=14"]
|
|
23
|
+
polars = ["polars>=0.20"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://kalshieye.com"
|
|
27
|
+
Documentation = "https://kalshieye.com/docs/bulk/"
|
|
28
|
+
Source = "https://github.com/KalshiEye/kalshieye-python"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
packages = ["kalshieye"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from kalshieye import Book, replay_book
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _row(ts, side, price, size, snapshot=False, ticker="T"):
|
|
5
|
+
return {"ts_recv": ts, "ticker": ticker, "is_snapshot": snapshot, "side": side, "price": price, "size": size}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_snapshot_then_deltas():
|
|
9
|
+
rows = [
|
|
10
|
+
_row(0, "yes", 0.40, 100.0, snapshot=True),
|
|
11
|
+
_row(0, "no", 0.55, 50.0, snapshot=True),
|
|
12
|
+
_row(1, "yes", 0.41, 20.0),
|
|
13
|
+
_row(2, "yes", 0.40, -100.0),
|
|
14
|
+
_row(3, "no", 0.55, -10.5),
|
|
15
|
+
]
|
|
16
|
+
states = list(replay_book(rows))
|
|
17
|
+
assert len(states) == 4 # one per message
|
|
18
|
+
_, ticker, bids, asks = states[-1]
|
|
19
|
+
assert ticker == "T"
|
|
20
|
+
assert bids == [(0.41, 20.0)]
|
|
21
|
+
assert asks == [(0.45, 39.5)] # a NO bid at 0.55 is a YES ask at 0.45
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_snapshot_replaces_the_book():
|
|
25
|
+
book = Book()
|
|
26
|
+
book.apply(_row(0, "yes", 0.30, 5.0, snapshot=True))
|
|
27
|
+
book.apply(_row(1, "yes", 0.31, 5.0))
|
|
28
|
+
book.apply(_row(2, "yes", 0.20, 7.0, snapshot=True))
|
|
29
|
+
assert book.bids() == [(0.20, 7.0)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_empty_snapshot_clears_the_book():
|
|
33
|
+
book = Book()
|
|
34
|
+
book.apply(_row(0, "yes", 0.30, 5.0, snapshot=True))
|
|
35
|
+
book.apply(_row(1, None, None, None, snapshot=True))
|
|
36
|
+
assert book.best() == (None, None)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_float_residue_does_not_leave_a_level():
|
|
40
|
+
# These sizes sum to exactly zero, but adding them as floats leaves about 1.4e-9.
|
|
41
|
+
sizes = [1121596.98, 1628450.55, 1595362.86, 1258899.31, -1569585.22, -4034724.48]
|
|
42
|
+
rows = [_row(0, "yes", 0.52, 10.0, snapshot=True), _row(0, "yes", 0.53, sizes[0], snapshot=True)]
|
|
43
|
+
rows += [_row(i, "yes", 0.53, size) for i, size in enumerate(sizes[1:], start=1)]
|
|
44
|
+
*_, (ts, ticker, bids, asks) = replay_book(rows)
|
|
45
|
+
assert bids == [(0.52, 10.0)]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_filters_one_ticker():
|
|
49
|
+
rows = [_row(0, "yes", 0.30, 5.0, snapshot=True, ticker="A"), _row(0, "yes", 0.60, 9.0, snapshot=True, ticker="B")]
|
|
50
|
+
assert [s[1] for s in replay_book(rows, ticker="b")] == ["B"]
|