pykalshi 0.1.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.
@@ -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
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: pykalshi
3
+ Version: 0.1.0
4
+ Summary: A typed Python client for the Kalshi prediction markets API with WebSocket streaming, automatic retries, and ergonomic interfaces
5
+ Author-email: Arsh Koneru <arshkon@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ArshKA/kalshi-api
8
+ Project-URL: Repository, https://github.com/ArshKA/kalshi-api
9
+ Project-URL: Documentation, https://github.com/ArshKA/kalshi-api#readme
10
+ Project-URL: Issues, https://github.com/ArshKA/kalshi-api/issues
11
+ Keywords: kalshi,prediction-markets,trading,api-client,websocket,finance,betting,forecasting,elections,event-contracts,binary-options,real-time,async,pydantic,orderbook,market-data
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Office/Business :: Financial :: Investment
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: requests>=2.31.0
28
+ Requires-Dist: pydantic>=2.0.0
29
+ Requires-Dist: cryptography>=41.0.0
30
+ Requires-Dist: python-dotenv>=1.0.0
31
+ Requires-Dist: websockets>=11.0
32
+ Provides-Extra: web
33
+ Requires-Dist: fastapi>=0.100.0; extra == "web"
34
+ Requires-Dist: uvicorn>=0.23.0; extra == "web"
35
+ Requires-Dist: aiofiles>=23.0.0; extra == "web"
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=7.0; extra == "dev"
38
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
39
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
40
+ Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
41
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
42
+ Requires-Dist: types-requests>=2.31.0; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # kalshi-api
46
+
47
+ A typed Python client for the [Kalshi](https://kalshi.com) prediction markets API with WebSocket streaming, automatic retries, and ergonomic interfaces.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install kalshi-api
53
+ ```
54
+
55
+ Create a `.env` file with your credentials from [kalshi.com](https://kalshi.com) → Account & Security → API Keys:
56
+
57
+ ```
58
+ KALSHI_API_KEY_ID=your-key-id
59
+ KALSHI_PRIVATE_KEY_PATH=/path/to/private-key.key
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ ```python
65
+ from kalshi_api import KalshiClient, Action, Side
66
+
67
+ client = KalshiClient()
68
+ user = client.get_user()
69
+
70
+ # Browse markets
71
+ markets = client.get_markets(status="open", limit=5)
72
+ market = client.get_market("KXBTC-25JAN15-B100000")
73
+
74
+ # Place an order
75
+ order = user.place_order(
76
+ market,
77
+ action=Action.BUY,
78
+ side=Side.YES,
79
+ count=10,
80
+ price=45 # cents
81
+ )
82
+
83
+ order.cancel() # if needed
84
+ ```
85
+
86
+ ## Usage
87
+
88
+ ### Portfolio
89
+
90
+ `KalshiClient` handles authentication. Call `get_user()` to access your portfolio:
91
+
92
+ ```python
93
+ client = KalshiClient() # Uses .env credentials
94
+ client = KalshiClient(demo=True) # Use demo environment
95
+
96
+ user = client.get_user()
97
+ user.get_balance() # BalanceModel with balance, portfolio_value
98
+ user.get_positions() # Your current positions
99
+ user.get_fills() # Your trade history
100
+ user.get_orders(status="resting") # Your open orders
101
+ ```
102
+
103
+ ### Markets
104
+
105
+ ```python
106
+ # Search markets
107
+ markets = client.get_markets(series_ticker="KXBTC", status="open")
108
+
109
+ # Get a specific market
110
+ market = client.get_market("KXBTC-25JAN15-B100000")
111
+ print(market.title, market.yes_bid, market.yes_ask)
112
+
113
+ # Market data
114
+ orderbook = market.get_orderbook()
115
+ trades = market.get_trades()
116
+ ```
117
+
118
+ ### Orders
119
+
120
+ ```python
121
+ from kalshi_api import Action, Side, OrderType
122
+
123
+ # Limit order (default)
124
+ order = user.place_order(market, Action.BUY, Side.YES, count=10, price=50)
125
+
126
+ # Market order
127
+ order = user.place_order(market, Action.BUY, Side.YES, count=10, order_type=OrderType.MARKET)
128
+
129
+ order.cancel()
130
+ ```
131
+
132
+ ### Real-time Streaming
133
+
134
+ Subscribe to live market data via WebSocket:
135
+
136
+ ```python
137
+ from kalshi_api import Feed
138
+
139
+ async def main():
140
+ async with Feed(client) as feed:
141
+ await feed.subscribe_ticker("KXBTC-25JAN15-B100000")
142
+ await feed.subscribe_orderbook("KXBTC-25JAN15-B100000")
143
+
144
+ async for msg in feed:
145
+ print(msg) # TickerMessage, OrderbookSnapshotMessage, etc.
146
+ ```
147
+
148
+ ### Error Handling
149
+
150
+ ```python
151
+ from kalshi_api import InsufficientFundsError, RateLimitError, KalshiAPIError
152
+
153
+ try:
154
+ user.place_order(...)
155
+ except InsufficientFundsError:
156
+ print("Not enough balance")
157
+ except RateLimitError:
158
+ pass # Client auto-retries with backoff
159
+ except KalshiAPIError as e:
160
+ print(f"{e.status_code}: {e.error_code}")
161
+ ```
162
+
163
+ ## Comparison with Official SDK
164
+
165
+ | Feature | kalshi-api | kalshi-python (official) |
166
+ |---------|------------|--------------------------|
167
+ | WebSocket streaming | ✓ | — |
168
+ | Automatic retry with backoff | ✓ | — |
169
+ | Rate limit handling | ✓ | — |
170
+ | Domain objects (`Market`, `Order`) | ✓ | — |
171
+ | Typed exceptions | ✓ | — |
172
+ | Local orderbook management | ✓ | — |
173
+ | Pydantic models | ✓ | — |
174
+ | Core trading API coverage | ✓ | ✓ |
175
+ | Full API coverage | — | ✓ |
176
+
177
+ The official SDK is auto-generated from the OpenAPI spec. This library adds the infrastructure needed for production trading: real-time data, error recovery, and ergonomic interfaces.
178
+
179
+ ## Links
180
+
181
+ - [Kalshi API Reference](https://trading-api.readme.io/reference)
182
+ - [kalshi-python (official SDK)](https://github.com/Kalshi/kalshi-python)
@@ -0,0 +1,20 @@
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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
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.
@@ -0,0 +1 @@
1
+ kalshi_api