pykalshi 0.1.0__py3-none-any.whl → 0.2.1__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.1.dist-info}/METADATA +12 -12
- pykalshi-0.2.1.dist-info/RECORD +35 -0
- pykalshi-0.2.1.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.1.dist-info}/WHEEL +0 -0
- {pykalshi-0.1.0.dist-info → pykalshi-0.2.1.dist-info}/licenses/LICENSE +0 -0
pykalshi/exceptions.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KalshiError(Exception):
|
|
5
|
+
"""Base exception for all Kalshi API errors."""
|
|
6
|
+
|
|
7
|
+
retryable: bool = False
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KalshiAPIError(KalshiError):
|
|
11
|
+
"""Raised when the API returns a non-200 response.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
status_code: HTTP status code.
|
|
15
|
+
message: Error message from the API.
|
|
16
|
+
error_code: Kalshi-specific error code (e.g., "insufficient_balance").
|
|
17
|
+
method: HTTP method of the failed request.
|
|
18
|
+
endpoint: API endpoint path.
|
|
19
|
+
request_body: Request payload (for POST/PUT), if available.
|
|
20
|
+
response_body: Raw response body for debugging.
|
|
21
|
+
retryable: Whether this error is safe to retry (e.g., 5xx, rate limits).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
retryable = True # 5xx errors are generally retryable
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
status_code: int,
|
|
29
|
+
message: str,
|
|
30
|
+
error_code: str | None = None,
|
|
31
|
+
*,
|
|
32
|
+
method: str | None = None,
|
|
33
|
+
endpoint: str | None = None,
|
|
34
|
+
request_body: dict[str, Any] | None = None,
|
|
35
|
+
response_body: dict[str, Any] | str | None = None,
|
|
36
|
+
):
|
|
37
|
+
# Build informative message
|
|
38
|
+
parts = [f"{status_code}: {message}"]
|
|
39
|
+
if error_code:
|
|
40
|
+
parts[0] = f"{status_code}: {message} ({error_code})"
|
|
41
|
+
if method and endpoint:
|
|
42
|
+
parts.append(f"[{method} {endpoint}]")
|
|
43
|
+
|
|
44
|
+
super().__init__(" ".join(parts))
|
|
45
|
+
|
|
46
|
+
self.status_code = status_code
|
|
47
|
+
self.message = message
|
|
48
|
+
self.error_code = error_code
|
|
49
|
+
self.method = method
|
|
50
|
+
self.endpoint = endpoint
|
|
51
|
+
self.request_body = request_body
|
|
52
|
+
self.response_body = response_body
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return (
|
|
56
|
+
f"{self.__class__.__name__}("
|
|
57
|
+
f"status_code={self.status_code}, "
|
|
58
|
+
f"message={self.message!r}, "
|
|
59
|
+
f"error_code={self.error_code!r}, "
|
|
60
|
+
f"endpoint={self.endpoint!r})"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AuthenticationError(KalshiAPIError):
|
|
65
|
+
"""Raised when authentication fails (401/403).
|
|
66
|
+
|
|
67
|
+
Common causes:
|
|
68
|
+
- Invalid or expired API key
|
|
69
|
+
- Malformed signature
|
|
70
|
+
- Clock skew (timestamp too old)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
retryable = False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class InsufficientFundsError(KalshiAPIError):
|
|
77
|
+
"""Raised when the order cannot be placed due to insufficient funds.
|
|
78
|
+
|
|
79
|
+
Check `request_body` for the order that was rejected.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
retryable = False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ResourceNotFoundError(KalshiAPIError):
|
|
86
|
+
"""Raised when a resource (market, order) is not found (404).
|
|
87
|
+
|
|
88
|
+
Check `endpoint` to see which resource was not found.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
retryable = False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RateLimitError(KalshiAPIError):
|
|
95
|
+
"""Raised when rate limit retries are exhausted (429).
|
|
96
|
+
|
|
97
|
+
Consider using a RateLimiter to proactively throttle requests.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
retryable = True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class OrderRejectedError(KalshiAPIError):
|
|
104
|
+
"""Raised when an order is rejected by the exchange.
|
|
105
|
+
|
|
106
|
+
Common causes:
|
|
107
|
+
- Market is closed or settled
|
|
108
|
+
- Invalid price (outside 1-99 range)
|
|
109
|
+
- Self-trade prevention triggered
|
|
110
|
+
- Post-only order would take liquidity
|
|
111
|
+
|
|
112
|
+
Check `request_body` for the rejected order details.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
retryable = False
|
pykalshi/exchange.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
from .models import ExchangeStatus, Announcement
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .client import KalshiClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Exchange:
|
|
10
|
+
"""Exchange status, schedule, and announcements."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, client: KalshiClient) -> None:
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
def get_status(self) -> ExchangeStatus:
|
|
16
|
+
"""Get current exchange operational status."""
|
|
17
|
+
data = self._client.get("/exchange/status")
|
|
18
|
+
return ExchangeStatus.model_validate(data)
|
|
19
|
+
|
|
20
|
+
def is_trading(self) -> bool:
|
|
21
|
+
"""Quick check if trading is currently active."""
|
|
22
|
+
return self.get_status().trading_active
|
|
23
|
+
|
|
24
|
+
def get_schedule(self) -> dict[str, Any]:
|
|
25
|
+
"""Get exchange trading schedule (raw format)."""
|
|
26
|
+
data = self._client.get("/exchange/schedule")
|
|
27
|
+
return data.get("schedule", {})
|
|
28
|
+
|
|
29
|
+
def get_announcements(self) -> list[Announcement]:
|
|
30
|
+
"""Get exchange-wide announcements."""
|
|
31
|
+
data = self._client.get("/exchange/announcements")
|
|
32
|
+
return [Announcement.model_validate(a) for a in data.get("announcements", [])]
|
|
33
|
+
|
|
34
|
+
def get_user_data_timestamp(self) -> int:
|
|
35
|
+
"""Get timestamp of last user data validation (Unix ms)."""
|
|
36
|
+
data = self._client.get("/exchange/user_data_timestamp")
|
|
37
|
+
return data.get("user_data_timestamp", 0)
|