pykalshi 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- pykalshi/__init__.py +144 -0
- pykalshi/api_keys.py +59 -0
- pykalshi/client.py +526 -0
- pykalshi/enums.py +54 -0
- pykalshi/events.py +87 -0
- pykalshi/exceptions.py +115 -0
- pykalshi/exchange.py +37 -0
- pykalshi/feed.py +592 -0
- pykalshi/markets.py +234 -0
- pykalshi/models.py +552 -0
- pykalshi/orderbook.py +146 -0
- pykalshi/orders.py +144 -0
- pykalshi/portfolio.py +542 -0
- pykalshi/py.typed +0 -0
- pykalshi/rate_limiter.py +171 -0
- {pykalshi-0.1.0.dist-info → pykalshi-0.2.0.dist-info}/METADATA +8 -8
- pykalshi-0.2.0.dist-info/RECORD +35 -0
- pykalshi-0.2.0.dist-info/top_level.txt +1 -0
- pykalshi-0.1.0.dist-info/RECORD +0 -20
- pykalshi-0.1.0.dist-info/top_level.txt +0 -1
- {pykalshi-0.1.0.dist-info → pykalshi-0.2.0.dist-info}/WHEEL +0 -0
- {pykalshi-0.1.0.dist-info → pykalshi-0.2.0.dist-info}/licenses/LICENSE +0 -0
pykalshi/rate_limiter.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate limiter for proactive request throttling.
|
|
3
|
+
|
|
4
|
+
Composable, optional, and easy to remove. Inject into KalshiClient
|
|
5
|
+
to prevent 429s rather than just retrying after them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from collections import deque
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RateLimiterProtocol(Protocol):
|
|
18
|
+
"""Protocol for rate limiters. Implement this to create custom limiters."""
|
|
19
|
+
|
|
20
|
+
def acquire(self, weight: int = 1) -> float:
|
|
21
|
+
"""Acquire permission to make a request.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
weight: Cost of this request (default 1).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Time waited in seconds (0 if no wait).
|
|
28
|
+
"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def update_from_headers(self, remaining: int | None, reset_at: int | None) -> None:
|
|
32
|
+
"""Update internal state from response headers.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
remaining: Requests remaining in current window.
|
|
36
|
+
reset_at: Unix timestamp when window resets.
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RateLimiter:
|
|
43
|
+
"""Token bucket rate limiter with sliding window.
|
|
44
|
+
|
|
45
|
+
Thread-safe. Proactively throttles requests to stay under limits.
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
limiter = RateLimiter(requests_per_second=10)
|
|
49
|
+
client = KalshiClient(..., rate_limiter=limiter)
|
|
50
|
+
|
|
51
|
+
# Or manual usage:
|
|
52
|
+
limiter.acquire() # Blocks if needed
|
|
53
|
+
response = make_request()
|
|
54
|
+
limiter.update_from_headers(
|
|
55
|
+
remaining=int(response.headers.get('X-RateLimit-Remaining')),
|
|
56
|
+
reset_at=int(response.headers.get('X-RateLimit-Reset')),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
requests_per_second: Target request rate.
|
|
61
|
+
burst: Maximum burst size (default: 2x requests_per_second).
|
|
62
|
+
min_spacing_ms: Minimum ms between requests (anti-burst).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
requests_per_second: float = 10.0
|
|
66
|
+
burst: int | None = None
|
|
67
|
+
min_spacing_ms: float = 0.0
|
|
68
|
+
|
|
69
|
+
_timestamps: deque = field(default_factory=deque, repr=False)
|
|
70
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
71
|
+
_last_request: float = field(default=0.0, repr=False)
|
|
72
|
+
|
|
73
|
+
# Server-reported state (updated from headers)
|
|
74
|
+
_server_remaining: int | None = field(default=None, repr=False)
|
|
75
|
+
_server_reset_at: int | None = field(default=None, repr=False)
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
if self.burst is None:
|
|
79
|
+
self.burst = max(1, int(self.requests_per_second * 2))
|
|
80
|
+
self._window_size = 1.0 # 1 second sliding window
|
|
81
|
+
|
|
82
|
+
def acquire(self, weight: int = 1) -> float:
|
|
83
|
+
"""Block until request is allowed. Returns wait time in seconds."""
|
|
84
|
+
with self._lock:
|
|
85
|
+
now = time.monotonic()
|
|
86
|
+
waited = 0.0
|
|
87
|
+
|
|
88
|
+
# Enforce minimum spacing
|
|
89
|
+
if self.min_spacing_ms > 0:
|
|
90
|
+
elapsed = (now - self._last_request) * 1000
|
|
91
|
+
if elapsed < self.min_spacing_ms:
|
|
92
|
+
sleep_time = (self.min_spacing_ms - elapsed) / 1000
|
|
93
|
+
time.sleep(sleep_time)
|
|
94
|
+
waited += sleep_time
|
|
95
|
+
now = time.monotonic()
|
|
96
|
+
|
|
97
|
+
# Clean old timestamps outside window
|
|
98
|
+
cutoff = now - self._window_size
|
|
99
|
+
while self._timestamps and self._timestamps[0] < cutoff:
|
|
100
|
+
self._timestamps.popleft()
|
|
101
|
+
|
|
102
|
+
# If at capacity, wait for oldest to expire
|
|
103
|
+
while len(self._timestamps) >= self.requests_per_second:
|
|
104
|
+
oldest = self._timestamps[0]
|
|
105
|
+
sleep_time = oldest + self._window_size - now
|
|
106
|
+
if sleep_time > 0:
|
|
107
|
+
time.sleep(sleep_time)
|
|
108
|
+
waited += sleep_time
|
|
109
|
+
now = time.monotonic()
|
|
110
|
+
# Re-clean after sleep
|
|
111
|
+
cutoff = now - self._window_size
|
|
112
|
+
while self._timestamps and self._timestamps[0] < cutoff:
|
|
113
|
+
self._timestamps.popleft()
|
|
114
|
+
|
|
115
|
+
# Check server-reported limits (be conservative)
|
|
116
|
+
if self._server_remaining is not None and self._server_remaining <= 1:
|
|
117
|
+
if self._server_reset_at is not None:
|
|
118
|
+
sleep_until = self._server_reset_at - time.time()
|
|
119
|
+
if sleep_until > 0:
|
|
120
|
+
time.sleep(sleep_until)
|
|
121
|
+
waited += sleep_until
|
|
122
|
+
now = time.monotonic()
|
|
123
|
+
self._server_remaining = None
|
|
124
|
+
|
|
125
|
+
# Record this request
|
|
126
|
+
for _ in range(weight):
|
|
127
|
+
self._timestamps.append(now)
|
|
128
|
+
self._last_request = now
|
|
129
|
+
|
|
130
|
+
return waited
|
|
131
|
+
|
|
132
|
+
def update_from_headers(
|
|
133
|
+
self, remaining: int | None, reset_at: int | None
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Update state from X-RateLimit-Remaining and X-RateLimit-Reset headers."""
|
|
136
|
+
with self._lock:
|
|
137
|
+
if remaining is not None:
|
|
138
|
+
self._server_remaining = remaining
|
|
139
|
+
if reset_at is not None:
|
|
140
|
+
self._server_reset_at = reset_at
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def current_rate(self) -> float:
|
|
144
|
+
"""Current request rate (requests in last second)."""
|
|
145
|
+
with self._lock:
|
|
146
|
+
now = time.monotonic()
|
|
147
|
+
cutoff = now - self._window_size
|
|
148
|
+
while self._timestamps and self._timestamps[0] < cutoff:
|
|
149
|
+
self._timestamps.popleft()
|
|
150
|
+
return len(self._timestamps)
|
|
151
|
+
|
|
152
|
+
def reset(self) -> None:
|
|
153
|
+
"""Clear all state. Useful for testing."""
|
|
154
|
+
with self._lock:
|
|
155
|
+
self._timestamps.clear()
|
|
156
|
+
self._last_request = 0.0
|
|
157
|
+
self._server_remaining = None
|
|
158
|
+
self._server_reset_at = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class NoOpRateLimiter:
|
|
163
|
+
"""Rate limiter that does nothing. For testing or opt-out."""
|
|
164
|
+
|
|
165
|
+
def acquire(self, weight: int = 1) -> float:
|
|
166
|
+
return 0.0
|
|
167
|
+
|
|
168
|
+
def update_from_headers(
|
|
169
|
+
self, remaining: int | None, reset_at: int | None
|
|
170
|
+
) -> None:
|
|
171
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pykalshi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A typed Python client for the Kalshi prediction markets API with WebSocket streaming, automatic retries, and ergonomic interfaces
|
|
5
5
|
Author-email: Arsh Koneru <arshkon@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -42,14 +42,14 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
42
42
|
Requires-Dist: types-requests>=2.31.0; extra == "dev"
|
|
43
43
|
Dynamic: license-file
|
|
44
44
|
|
|
45
|
-
#
|
|
45
|
+
# pykalshi
|
|
46
46
|
|
|
47
47
|
A typed Python client for the [Kalshi](https://kalshi.com) prediction markets API with WebSocket streaming, automatic retries, and ergonomic interfaces.
|
|
48
48
|
|
|
49
49
|
## Installation
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
pip install
|
|
52
|
+
pip install pykalshi
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Create a `.env` file with your credentials from [kalshi.com](https://kalshi.com) → Account & Security → API Keys:
|
|
@@ -62,7 +62,7 @@ KALSHI_PRIVATE_KEY_PATH=/path/to/private-key.key
|
|
|
62
62
|
## Quick Start
|
|
63
63
|
|
|
64
64
|
```python
|
|
65
|
-
from
|
|
65
|
+
from pykalshi import KalshiClient, Action, Side
|
|
66
66
|
|
|
67
67
|
client = KalshiClient()
|
|
68
68
|
user = client.get_user()
|
|
@@ -118,7 +118,7 @@ trades = market.get_trades()
|
|
|
118
118
|
### Orders
|
|
119
119
|
|
|
120
120
|
```python
|
|
121
|
-
from
|
|
121
|
+
from pykalshi import Action, Side, OrderType
|
|
122
122
|
|
|
123
123
|
# Limit order (default)
|
|
124
124
|
order = user.place_order(market, Action.BUY, Side.YES, count=10, price=50)
|
|
@@ -134,7 +134,7 @@ order.cancel()
|
|
|
134
134
|
Subscribe to live market data via WebSocket:
|
|
135
135
|
|
|
136
136
|
```python
|
|
137
|
-
from
|
|
137
|
+
from pykalshi import Feed
|
|
138
138
|
|
|
139
139
|
async def main():
|
|
140
140
|
async with Feed(client) as feed:
|
|
@@ -148,7 +148,7 @@ async def main():
|
|
|
148
148
|
### Error Handling
|
|
149
149
|
|
|
150
150
|
```python
|
|
151
|
-
from
|
|
151
|
+
from pykalshi import InsufficientFundsError, RateLimitError, KalshiAPIError
|
|
152
152
|
|
|
153
153
|
try:
|
|
154
154
|
user.place_order(...)
|
|
@@ -162,7 +162,7 @@ except KalshiAPIError as e:
|
|
|
162
162
|
|
|
163
163
|
## Comparison with Official SDK
|
|
164
164
|
|
|
165
|
-
| Feature |
|
|
165
|
+
| Feature | pykalshi | kalshi-python (official) |
|
|
166
166
|
|---------|------------|--------------------------|
|
|
167
167
|
| WebSocket streaming | ✓ | — |
|
|
168
168
|
| Automatic retry with backoff | ✓ | — |
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
kalshi_api/__init__.py,sha256=H873JDfBga0KEi-G-EJOPBljMZiOTOcpw7x2Qaeru9w,3018
|
|
2
|
+
kalshi_api/api_keys.py,sha256=HMn5bzKo5yX8edcKpJy20NbrR2z7vUa5PJk8TB0iBwU,1922
|
|
3
|
+
kalshi_api/client.py,sha256=4IA-f8OSMFYvxf8tmKL06-2gb7GdDISHDLUPdCtF9ac,19362
|
|
4
|
+
kalshi_api/enums.py,sha256=9YF4ooA1RTprI08-NzqVZoPXISAmHDWgv3j0971BRyI,1127
|
|
5
|
+
kalshi_api/events.py,sha256=0eC9xlqBblwxJdmdiFwjuiL1XUbw6eqcK-vCjoUUgqA,2683
|
|
6
|
+
kalshi_api/exceptions.py,sha256=ikv97ztZwYWKr4SZ0EUTtR50KZQFjbpuI5vMMpPxiW4,3143
|
|
7
|
+
kalshi_api/exchange.py,sha256=39-Zx-VZ6ezkOeB4E9fP4yhTX-qoO4FRSkpcEGmhG60,1359
|
|
8
|
+
kalshi_api/feed.py,sha256=zhPGaER7oBMAHZgpxdppvWDkC-NLn6W_ORnLCkCQh_8,19470
|
|
9
|
+
kalshi_api/markets.py,sha256=ben3yUqpW2Jn1bp67lJ7gyRjUHtwPTZkKQxyH_ao3ZA,6901
|
|
10
|
+
kalshi_api/models.py,sha256=hQdkJMK4sFYXiDEjjEwsEY4skBXEzN3IuJFpzojF8Lw,16038
|
|
11
|
+
kalshi_api/orderbook.py,sha256=40UIqEz-pgM49YdFNTXAd63Rw2hCaEq-sQPPy9l3q_E,4830
|
|
12
|
+
kalshi_api/orders.py,sha256=F4mk0PPS_cBzDRbg0x50GpQb-AqfJPES2Vw45Zes3Rc,3726
|
|
13
|
+
kalshi_api/portfolio.py,sha256=JgkgGTSEytn3OrIRbdci-bvkBOtRmxBK69uFuyYwArI,19937
|
|
14
|
+
kalshi_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
kalshi_api/rate_limiter.py,sha256=UJPjl-YUtLdVwQcaoq97ZH1oeS-IhqtV8K5G6WpUyDg,5901
|
|
16
|
+
pykalshi/__init__.py,sha256=H873JDfBga0KEi-G-EJOPBljMZiOTOcpw7x2Qaeru9w,3018
|
|
17
|
+
pykalshi/api_keys.py,sha256=HMn5bzKo5yX8edcKpJy20NbrR2z7vUa5PJk8TB0iBwU,1922
|
|
18
|
+
pykalshi/client.py,sha256=4IA-f8OSMFYvxf8tmKL06-2gb7GdDISHDLUPdCtF9ac,19362
|
|
19
|
+
pykalshi/enums.py,sha256=9YF4ooA1RTprI08-NzqVZoPXISAmHDWgv3j0971BRyI,1127
|
|
20
|
+
pykalshi/events.py,sha256=0eC9xlqBblwxJdmdiFwjuiL1XUbw6eqcK-vCjoUUgqA,2683
|
|
21
|
+
pykalshi/exceptions.py,sha256=ikv97ztZwYWKr4SZ0EUTtR50KZQFjbpuI5vMMpPxiW4,3143
|
|
22
|
+
pykalshi/exchange.py,sha256=39-Zx-VZ6ezkOeB4E9fP4yhTX-qoO4FRSkpcEGmhG60,1359
|
|
23
|
+
pykalshi/feed.py,sha256=zhPGaER7oBMAHZgpxdppvWDkC-NLn6W_ORnLCkCQh_8,19470
|
|
24
|
+
pykalshi/markets.py,sha256=ben3yUqpW2Jn1bp67lJ7gyRjUHtwPTZkKQxyH_ao3ZA,6901
|
|
25
|
+
pykalshi/models.py,sha256=hQdkJMK4sFYXiDEjjEwsEY4skBXEzN3IuJFpzojF8Lw,16038
|
|
26
|
+
pykalshi/orderbook.py,sha256=40UIqEz-pgM49YdFNTXAd63Rw2hCaEq-sQPPy9l3q_E,4830
|
|
27
|
+
pykalshi/orders.py,sha256=F4mk0PPS_cBzDRbg0x50GpQb-AqfJPES2Vw45Zes3Rc,3726
|
|
28
|
+
pykalshi/portfolio.py,sha256=JgkgGTSEytn3OrIRbdci-bvkBOtRmxBK69uFuyYwArI,19937
|
|
29
|
+
pykalshi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
pykalshi/rate_limiter.py,sha256=UJPjl-YUtLdVwQcaoq97ZH1oeS-IhqtV8K5G6WpUyDg,5901
|
|
31
|
+
pykalshi-0.2.0.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
|
|
32
|
+
pykalshi-0.2.0.dist-info/METADATA,sha256=SVfODyFs1tnfAldVPLhHLe9Jlcx8lxZahQjM4bwU3B0,5589
|
|
33
|
+
pykalshi-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
34
|
+
pykalshi-0.2.0.dist-info/top_level.txt,sha256=d5Rnt2Wuf3HxyeG6hwGi8-g9G6_KLETpyJ5zExXO1M4,9
|
|
35
|
+
pykalshi-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pykalshi
|
pykalshi-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
kalshi_api/__init__.py,sha256=H873JDfBga0KEi-G-EJOPBljMZiOTOcpw7x2Qaeru9w,3018
|
|
2
|
-
kalshi_api/api_keys.py,sha256=HMn5bzKo5yX8edcKpJy20NbrR2z7vUa5PJk8TB0iBwU,1922
|
|
3
|
-
kalshi_api/client.py,sha256=4IA-f8OSMFYvxf8tmKL06-2gb7GdDISHDLUPdCtF9ac,19362
|
|
4
|
-
kalshi_api/enums.py,sha256=9YF4ooA1RTprI08-NzqVZoPXISAmHDWgv3j0971BRyI,1127
|
|
5
|
-
kalshi_api/events.py,sha256=0eC9xlqBblwxJdmdiFwjuiL1XUbw6eqcK-vCjoUUgqA,2683
|
|
6
|
-
kalshi_api/exceptions.py,sha256=ikv97ztZwYWKr4SZ0EUTtR50KZQFjbpuI5vMMpPxiW4,3143
|
|
7
|
-
kalshi_api/exchange.py,sha256=39-Zx-VZ6ezkOeB4E9fP4yhTX-qoO4FRSkpcEGmhG60,1359
|
|
8
|
-
kalshi_api/feed.py,sha256=zhPGaER7oBMAHZgpxdppvWDkC-NLn6W_ORnLCkCQh_8,19470
|
|
9
|
-
kalshi_api/markets.py,sha256=ben3yUqpW2Jn1bp67lJ7gyRjUHtwPTZkKQxyH_ao3ZA,6901
|
|
10
|
-
kalshi_api/models.py,sha256=hQdkJMK4sFYXiDEjjEwsEY4skBXEzN3IuJFpzojF8Lw,16038
|
|
11
|
-
kalshi_api/orderbook.py,sha256=40UIqEz-pgM49YdFNTXAd63Rw2hCaEq-sQPPy9l3q_E,4830
|
|
12
|
-
kalshi_api/orders.py,sha256=F4mk0PPS_cBzDRbg0x50GpQb-AqfJPES2Vw45Zes3Rc,3726
|
|
13
|
-
kalshi_api/portfolio.py,sha256=JgkgGTSEytn3OrIRbdci-bvkBOtRmxBK69uFuyYwArI,19937
|
|
14
|
-
kalshi_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
kalshi_api/rate_limiter.py,sha256=UJPjl-YUtLdVwQcaoq97ZH1oeS-IhqtV8K5G6WpUyDg,5901
|
|
16
|
-
pykalshi-0.1.0.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
|
|
17
|
-
pykalshi-0.1.0.dist-info/METADATA,sha256=qYNSSrd3cUElCZt-FuL0MoSifw7NdKoQlx6lpjcfYRo,5603
|
|
18
|
-
pykalshi-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
19
|
-
pykalshi-0.1.0.dist-info/top_level.txt,sha256=j1cmqf7vUVoke4KfEg_he913wTsgN5nT0Ky5P3P7Dik,11
|
|
20
|
-
pykalshi-0.1.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
kalshi_api
|
|
File without changes
|
|
File without changes
|