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.
- kalshi_api/__init__.py +144 -0
- kalshi_api/api_keys.py +59 -0
- kalshi_api/client.py +526 -0
- kalshi_api/enums.py +54 -0
- kalshi_api/events.py +87 -0
- kalshi_api/exceptions.py +115 -0
- kalshi_api/exchange.py +37 -0
- kalshi_api/feed.py +592 -0
- kalshi_api/markets.py +234 -0
- kalshi_api/models.py +552 -0
- kalshi_api/orderbook.py +146 -0
- kalshi_api/orders.py +144 -0
- kalshi_api/portfolio.py +542 -0
- kalshi_api/py.typed +0 -0
- kalshi_api/rate_limiter.py +171 -0
- pykalshi-0.1.0.dist-info/METADATA +182 -0
- pykalshi-0.1.0.dist-info/RECORD +20 -0
- pykalshi-0.1.0.dist-info/WHEEL +5 -0
- pykalshi-0.1.0.dist-info/licenses/LICENSE +21 -0
- pykalshi-0.1.0.dist-info/top_level.txt +1 -0
kalshi_api/orders.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from .models import OrderModel
|
|
4
|
+
from .enums import OrderStatus, Action, Side, OrderType
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .client import KalshiClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Order:
|
|
11
|
+
"""Represents a Kalshi order.
|
|
12
|
+
|
|
13
|
+
Key fields are exposed as typed properties for IDE support.
|
|
14
|
+
All other OrderModel fields are accessible via attribute delegation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, client: KalshiClient, data: OrderModel) -> None:
|
|
18
|
+
self._client = client
|
|
19
|
+
self.data = data
|
|
20
|
+
|
|
21
|
+
# --- Typed properties for core fields ---
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def order_id(self) -> str:
|
|
25
|
+
return self.data.order_id
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def ticker(self) -> str:
|
|
29
|
+
return self.data.ticker
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def status(self) -> OrderStatus:
|
|
33
|
+
return self.data.status
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def action(self) -> Action | None:
|
|
37
|
+
return self.data.action
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def side(self) -> Side | None:
|
|
41
|
+
return self.data.side
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def type(self) -> OrderType | None:
|
|
45
|
+
return self.data.type
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def yes_price(self) -> int | None:
|
|
49
|
+
return self.data.yes_price
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def no_price(self) -> int | None:
|
|
53
|
+
return self.data.no_price
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def initial_count(self) -> int | None:
|
|
57
|
+
return self.data.initial_count
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def fill_count(self) -> int | None:
|
|
61
|
+
return self.data.fill_count
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def remaining_count(self) -> int | None:
|
|
65
|
+
return self.data.remaining_count
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def created_time(self) -> str | None:
|
|
69
|
+
return self.data.created_time
|
|
70
|
+
|
|
71
|
+
# --- Domain logic ---
|
|
72
|
+
|
|
73
|
+
def cancel(self) -> Order:
|
|
74
|
+
"""Cancel this order.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Self with updated data (status will be CANCELED).
|
|
78
|
+
"""
|
|
79
|
+
updated = self._client.portfolio.cancel_order(self.order_id)
|
|
80
|
+
self.data = updated.data
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def amend(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
count: int | None = None,
|
|
87
|
+
yes_price: int | None = None,
|
|
88
|
+
no_price: int | None = None,
|
|
89
|
+
) -> Order:
|
|
90
|
+
"""Amend this order's price or count.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
count: New total contract count.
|
|
94
|
+
yes_price: New YES price in cents.
|
|
95
|
+
no_price: New NO price in cents (converted to yes_price internally).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Self with updated data.
|
|
99
|
+
"""
|
|
100
|
+
updated = self._client.portfolio.amend_order(
|
|
101
|
+
self.order_id,
|
|
102
|
+
count=count,
|
|
103
|
+
yes_price=yes_price,
|
|
104
|
+
no_price=no_price,
|
|
105
|
+
)
|
|
106
|
+
self.data = updated.data
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def decrease(self, reduce_by: int) -> Order:
|
|
110
|
+
"""Decrease the remaining count of this order.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
reduce_by: Number of contracts to reduce by.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Self with updated data.
|
|
117
|
+
"""
|
|
118
|
+
updated = self._client.portfolio.decrease_order(self.order_id, reduce_by)
|
|
119
|
+
self.data = updated.data
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def refresh(self) -> Order:
|
|
123
|
+
"""Re-fetch this order's current state from the API.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Self with updated data.
|
|
127
|
+
"""
|
|
128
|
+
updated = self._client.portfolio.get_order(self.order_id)
|
|
129
|
+
self.data = updated.data
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def __getattr__(self, name: str):
|
|
133
|
+
return getattr(self.data, name)
|
|
134
|
+
|
|
135
|
+
def __eq__(self, other: object) -> bool:
|
|
136
|
+
if not isinstance(other, Order):
|
|
137
|
+
return NotImplemented
|
|
138
|
+
return self.data.order_id == other.data.order_id
|
|
139
|
+
|
|
140
|
+
def __hash__(self) -> int:
|
|
141
|
+
return hash(self.data.order_id)
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
return f"<Order {self.data.order_id} {self.data.status.value}>"
|
kalshi_api/portfolio.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from .orders import Order
|
|
4
|
+
from .enums import Action, Side, OrderType, OrderStatus, TimeInForce, SelfTradePrevention
|
|
5
|
+
from .models import (
|
|
6
|
+
OrderModel, BalanceModel, PositionModel, FillModel,
|
|
7
|
+
SettlementModel, QueuePositionModel, OrderGroupModel,
|
|
8
|
+
SubaccountModel, SubaccountBalanceModel, SubaccountTransferModel,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .client import KalshiClient
|
|
13
|
+
from .markets import Market
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Portfolio:
|
|
17
|
+
"""Authenticated user's portfolio and trading operations."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: KalshiClient) -> None:
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def get_balance(self) -> BalanceModel:
|
|
23
|
+
"""Get portfolio balance. Values are in cents."""
|
|
24
|
+
data = self._client.get("/portfolio/balance")
|
|
25
|
+
return BalanceModel.model_validate(data)
|
|
26
|
+
|
|
27
|
+
def place_order(
|
|
28
|
+
self,
|
|
29
|
+
ticker: str | Market,
|
|
30
|
+
action: Action,
|
|
31
|
+
side: Side,
|
|
32
|
+
count: int,
|
|
33
|
+
order_type: OrderType = OrderType.LIMIT,
|
|
34
|
+
*,
|
|
35
|
+
yes_price: int | None = None,
|
|
36
|
+
no_price: int | None = None,
|
|
37
|
+
client_order_id: str | None = None,
|
|
38
|
+
time_in_force: TimeInForce | None = None,
|
|
39
|
+
post_only: bool = False,
|
|
40
|
+
reduce_only: bool = False,
|
|
41
|
+
expiration_ts: int | None = None,
|
|
42
|
+
buy_max_cost: int | None = None,
|
|
43
|
+
self_trade_prevention: SelfTradePrevention | None = None,
|
|
44
|
+
order_group_id: str | None = None,
|
|
45
|
+
) -> Order:
|
|
46
|
+
"""Place an order on a market.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
ticker: Market ticker string or Market object.
|
|
50
|
+
action: BUY or SELL.
|
|
51
|
+
side: YES or NO.
|
|
52
|
+
count: Number of contracts.
|
|
53
|
+
order_type: LIMIT or MARKET.
|
|
54
|
+
yes_price: Price in cents (1-99) for the YES side.
|
|
55
|
+
no_price: Price in cents (1-99) for the NO side.
|
|
56
|
+
Converted to yes_price internally (yes_price = 100 - no_price).
|
|
57
|
+
client_order_id: Idempotency key. Resubmitting returns existing order.
|
|
58
|
+
time_in_force: GTC (default), IOC (immediate-or-cancel), FOK (fill-or-kill).
|
|
59
|
+
post_only: If True, reject order if it would take liquidity. Essential for market makers.
|
|
60
|
+
reduce_only: If True, only reduce existing position, never increase.
|
|
61
|
+
expiration_ts: Unix timestamp when order auto-cancels.
|
|
62
|
+
buy_max_cost: Maximum total cost in cents. Protects against slippage.
|
|
63
|
+
self_trade_prevention: Behavior on self-cross (CANCEL_TAKER or CANCEL_MAKER).
|
|
64
|
+
order_group_id: Link to an order group for OCO/bracket strategies.
|
|
65
|
+
"""
|
|
66
|
+
if yes_price is not None and no_price is not None:
|
|
67
|
+
raise ValueError("Specify yes_price or no_price, not both")
|
|
68
|
+
if yes_price is None and no_price is None and order_type == OrderType.LIMIT:
|
|
69
|
+
raise ValueError("Limit orders require yes_price or no_price")
|
|
70
|
+
|
|
71
|
+
if no_price is not None:
|
|
72
|
+
yes_price = 100 - no_price
|
|
73
|
+
|
|
74
|
+
ticker_str = ticker if isinstance(ticker, str) else ticker.ticker
|
|
75
|
+
|
|
76
|
+
order_data: dict = {
|
|
77
|
+
"ticker": ticker_str,
|
|
78
|
+
"action": action.value,
|
|
79
|
+
"side": side.value,
|
|
80
|
+
"count": count,
|
|
81
|
+
"type": order_type.value,
|
|
82
|
+
}
|
|
83
|
+
if yes_price is not None:
|
|
84
|
+
order_data["yes_price"] = yes_price
|
|
85
|
+
if client_order_id is not None:
|
|
86
|
+
order_data["client_order_id"] = client_order_id
|
|
87
|
+
if time_in_force is not None:
|
|
88
|
+
order_data["time_in_force"] = time_in_force.value
|
|
89
|
+
if post_only:
|
|
90
|
+
order_data["post_only"] = True
|
|
91
|
+
if reduce_only:
|
|
92
|
+
order_data["reduce_only"] = True
|
|
93
|
+
if expiration_ts is not None:
|
|
94
|
+
order_data["expiration_ts"] = expiration_ts
|
|
95
|
+
if buy_max_cost is not None:
|
|
96
|
+
order_data["buy_max_cost"] = buy_max_cost
|
|
97
|
+
if self_trade_prevention is not None:
|
|
98
|
+
order_data["self_trade_prevention_type"] = self_trade_prevention.value
|
|
99
|
+
if order_group_id is not None:
|
|
100
|
+
order_data["order_group_id"] = order_group_id
|
|
101
|
+
|
|
102
|
+
response = self._client.post("/portfolio/orders", order_data)
|
|
103
|
+
model = OrderModel.model_validate(response["order"])
|
|
104
|
+
return Order(self._client, model)
|
|
105
|
+
|
|
106
|
+
def cancel_order(self, order_id: str) -> Order:
|
|
107
|
+
"""Cancel a resting order.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
order_id: ID of the order to cancel.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
The canceled Order with updated status.
|
|
114
|
+
"""
|
|
115
|
+
response = self._client.delete(f"/portfolio/orders/{order_id}")
|
|
116
|
+
model = OrderModel.model_validate(response["order"])
|
|
117
|
+
return Order(self._client, model)
|
|
118
|
+
|
|
119
|
+
def amend_order(
|
|
120
|
+
self,
|
|
121
|
+
order_id: str,
|
|
122
|
+
*,
|
|
123
|
+
count: int | None = None,
|
|
124
|
+
yes_price: int | None = None,
|
|
125
|
+
no_price: int | None = None,
|
|
126
|
+
) -> Order:
|
|
127
|
+
"""Amend a resting order's price or count.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
order_id: ID of the order to amend.
|
|
131
|
+
count: New total contract count.
|
|
132
|
+
yes_price: New YES price in cents.
|
|
133
|
+
no_price: New NO price in cents. Converted to yes_price internally.
|
|
134
|
+
"""
|
|
135
|
+
if yes_price is not None and no_price is not None:
|
|
136
|
+
raise ValueError("Specify yes_price or no_price, not both")
|
|
137
|
+
|
|
138
|
+
if no_price is not None:
|
|
139
|
+
yes_price = 100 - no_price
|
|
140
|
+
|
|
141
|
+
body: dict = {}
|
|
142
|
+
if count is not None:
|
|
143
|
+
body["count"] = count
|
|
144
|
+
if yes_price is not None:
|
|
145
|
+
body["yes_price"] = yes_price
|
|
146
|
+
|
|
147
|
+
if not body:
|
|
148
|
+
raise ValueError("Must specify at least one of count, yes_price, or no_price")
|
|
149
|
+
|
|
150
|
+
response = self._client.post(f"/portfolio/orders/{order_id}/amend", body)
|
|
151
|
+
model = OrderModel.model_validate(response["order"])
|
|
152
|
+
return Order(self._client, model)
|
|
153
|
+
|
|
154
|
+
def decrease_order(self, order_id: str, reduce_by: int) -> Order:
|
|
155
|
+
"""Decrease the remaining count of a resting order.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
order_id: ID of the order to decrease.
|
|
159
|
+
reduce_by: Number of contracts to reduce by.
|
|
160
|
+
"""
|
|
161
|
+
response = self._client.post(
|
|
162
|
+
f"/portfolio/orders/{order_id}/decrease", {"reduce_by": reduce_by}
|
|
163
|
+
)
|
|
164
|
+
model = OrderModel.model_validate(response["order"])
|
|
165
|
+
return Order(self._client, model)
|
|
166
|
+
|
|
167
|
+
def get_orders(
|
|
168
|
+
self,
|
|
169
|
+
status: OrderStatus | None = None,
|
|
170
|
+
ticker: str | None = None,
|
|
171
|
+
limit: int = 100,
|
|
172
|
+
cursor: str | None = None,
|
|
173
|
+
fetch_all: bool = False,
|
|
174
|
+
) -> list[Order]:
|
|
175
|
+
"""Get list of orders.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
status: Filter by order status.
|
|
179
|
+
ticker: Filter by market ticker.
|
|
180
|
+
limit: Maximum results per page (default 100).
|
|
181
|
+
cursor: Pagination cursor for fetching next page.
|
|
182
|
+
fetch_all: If True, automatically fetch all pages.
|
|
183
|
+
"""
|
|
184
|
+
params = {
|
|
185
|
+
"limit": limit,
|
|
186
|
+
"status": status.value if status is not None else None,
|
|
187
|
+
"ticker": ticker,
|
|
188
|
+
"cursor": cursor,
|
|
189
|
+
}
|
|
190
|
+
data = self._client.paginated_get("/portfolio/orders", "orders", params, fetch_all)
|
|
191
|
+
return [Order(self._client, OrderModel.model_validate(d)) for d in data]
|
|
192
|
+
|
|
193
|
+
def get_order(self, order_id: str) -> Order:
|
|
194
|
+
"""Get a single order by ID."""
|
|
195
|
+
response = self._client.get(f"/portfolio/orders/{order_id}")
|
|
196
|
+
model = OrderModel.model_validate(response["order"])
|
|
197
|
+
return Order(self._client, model)
|
|
198
|
+
|
|
199
|
+
def get_positions(
|
|
200
|
+
self,
|
|
201
|
+
ticker: str | None = None,
|
|
202
|
+
event_ticker: str | None = None,
|
|
203
|
+
count_filter: str | None = None,
|
|
204
|
+
limit: int = 100,
|
|
205
|
+
cursor: str | None = None,
|
|
206
|
+
fetch_all: bool = False,
|
|
207
|
+
) -> list[PositionModel]:
|
|
208
|
+
"""Get portfolio positions.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
ticker: Filter by specific market ticker.
|
|
212
|
+
event_ticker: Filter by event ticker.
|
|
213
|
+
count_filter: Filter positions with non-zero values.
|
|
214
|
+
Options: "position", "total_traded", or both comma-separated.
|
|
215
|
+
limit: Maximum positions per page (default 100, max 1000).
|
|
216
|
+
cursor: Pagination cursor for fetching next page.
|
|
217
|
+
fetch_all: If True, automatically fetch all pages.
|
|
218
|
+
"""
|
|
219
|
+
params = {
|
|
220
|
+
"limit": limit,
|
|
221
|
+
"ticker": ticker,
|
|
222
|
+
"event_ticker": event_ticker,
|
|
223
|
+
"count_filter": count_filter,
|
|
224
|
+
"cursor": cursor,
|
|
225
|
+
}
|
|
226
|
+
data = self._client.paginated_get("/portfolio/positions", "market_positions", params, fetch_all)
|
|
227
|
+
return [PositionModel.model_validate(p) for p in data]
|
|
228
|
+
|
|
229
|
+
def get_fills(
|
|
230
|
+
self,
|
|
231
|
+
ticker: str | None = None,
|
|
232
|
+
order_id: str | None = None,
|
|
233
|
+
min_ts: int | None = None,
|
|
234
|
+
max_ts: int | None = None,
|
|
235
|
+
limit: int = 100,
|
|
236
|
+
cursor: str | None = None,
|
|
237
|
+
fetch_all: bool = False,
|
|
238
|
+
) -> list[FillModel]:
|
|
239
|
+
"""Get trade fills (executed trades).
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
ticker: Filter by market ticker.
|
|
243
|
+
order_id: Filter by specific order ID.
|
|
244
|
+
min_ts: Minimum timestamp (Unix seconds).
|
|
245
|
+
max_ts: Maximum timestamp (Unix seconds).
|
|
246
|
+
limit: Maximum fills per page (default 100, max 200).
|
|
247
|
+
cursor: Pagination cursor for fetching next page.
|
|
248
|
+
fetch_all: If True, automatically fetch all pages.
|
|
249
|
+
"""
|
|
250
|
+
params = {
|
|
251
|
+
"limit": limit,
|
|
252
|
+
"ticker": ticker,
|
|
253
|
+
"order_id": order_id,
|
|
254
|
+
"min_ts": min_ts,
|
|
255
|
+
"max_ts": max_ts,
|
|
256
|
+
"cursor": cursor,
|
|
257
|
+
}
|
|
258
|
+
data = self._client.paginated_get("/portfolio/fills", "fills", params, fetch_all)
|
|
259
|
+
return [FillModel.model_validate(f) for f in data]
|
|
260
|
+
|
|
261
|
+
# --- Batch Operations ---
|
|
262
|
+
|
|
263
|
+
def batch_place_orders(self, orders: list[dict]) -> list[Order]:
|
|
264
|
+
"""Place multiple orders atomically.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
orders: List of order dicts with keys: ticker, action, side, count,
|
|
268
|
+
type, yes_price/no_price, and optional advanced params.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of created Order objects.
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
orders = [
|
|
275
|
+
{"ticker": "KXBTC", "action": "buy", "side": "yes", "count": 10, "type": "limit", "yes_price": 45},
|
|
276
|
+
{"ticker": "KXBTC", "action": "buy", "side": "no", "count": 10, "type": "limit", "yes_price": 55},
|
|
277
|
+
]
|
|
278
|
+
results = portfolio.batch_place_orders(orders)
|
|
279
|
+
"""
|
|
280
|
+
response = self._client.post("/portfolio/orders/batched", {"orders": orders})
|
|
281
|
+
return [Order(self._client, OrderModel.model_validate(o)) for o in response.get("orders", [])]
|
|
282
|
+
|
|
283
|
+
def batch_cancel_orders(self, order_ids: list[str]) -> list[Order]:
|
|
284
|
+
"""Cancel multiple orders atomically.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
order_ids: List of order IDs to cancel.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of canceled Order objects.
|
|
291
|
+
"""
|
|
292
|
+
response = self._client.post(
|
|
293
|
+
"/portfolio/orders/batched/cancel",
|
|
294
|
+
{"order_ids": order_ids}
|
|
295
|
+
)
|
|
296
|
+
return [Order(self._client, OrderModel.model_validate(o)) for o in response.get("orders", [])]
|
|
297
|
+
|
|
298
|
+
# --- Queue Position ---
|
|
299
|
+
|
|
300
|
+
def get_queue_position(self, order_id: str) -> QueuePositionModel:
|
|
301
|
+
"""Get queue position for a single resting order.
|
|
302
|
+
|
|
303
|
+
Returns 0-indexed position in the queue at the order's price level.
|
|
304
|
+
Position 0 means you're first in line to be filled.
|
|
305
|
+
"""
|
|
306
|
+
response = self._client.get(f"/portfolio/orders/{order_id}/queue_position")
|
|
307
|
+
return QueuePositionModel(
|
|
308
|
+
order_id=order_id,
|
|
309
|
+
queue_position=response.get("queue_position", 0)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def get_queue_positions(self, order_ids: list[str]) -> list[QueuePositionModel]:
|
|
313
|
+
"""Get queue positions for multiple resting orders.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
order_ids: List of order IDs.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
List of QueuePositionModel objects.
|
|
320
|
+
"""
|
|
321
|
+
response = self._client.post(
|
|
322
|
+
"/portfolio/orders/queue_positions",
|
|
323
|
+
{"order_ids": order_ids}
|
|
324
|
+
)
|
|
325
|
+
return [
|
|
326
|
+
QueuePositionModel.model_validate(qp)
|
|
327
|
+
for qp in response.get("queue_positions", [])
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
# --- Settlements ---
|
|
331
|
+
|
|
332
|
+
def get_settlements(
|
|
333
|
+
self,
|
|
334
|
+
ticker: str | None = None,
|
|
335
|
+
event_ticker: str | None = None,
|
|
336
|
+
limit: int = 100,
|
|
337
|
+
cursor: str | None = None,
|
|
338
|
+
fetch_all: bool = False,
|
|
339
|
+
) -> list[SettlementModel]:
|
|
340
|
+
"""Get settlement records for resolved positions.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
ticker: Filter by market ticker.
|
|
344
|
+
event_ticker: Filter by event ticker.
|
|
345
|
+
limit: Maximum settlements per page (default 100).
|
|
346
|
+
cursor: Pagination cursor.
|
|
347
|
+
fetch_all: If True, automatically fetch all pages.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
List of settlement records showing resolution outcomes.
|
|
351
|
+
"""
|
|
352
|
+
params = {
|
|
353
|
+
"limit": limit,
|
|
354
|
+
"ticker": ticker,
|
|
355
|
+
"event_ticker": event_ticker,
|
|
356
|
+
"cursor": cursor,
|
|
357
|
+
}
|
|
358
|
+
data = self._client.paginated_get("/portfolio/settlements", "settlements", params, fetch_all)
|
|
359
|
+
return [SettlementModel.model_validate(s) for s in data]
|
|
360
|
+
|
|
361
|
+
def get_resting_order_value(self) -> int:
|
|
362
|
+
"""Get total value of all resting orders in cents.
|
|
363
|
+
|
|
364
|
+
NOTE: This endpoint is FCM-only (institutional accounts).
|
|
365
|
+
Regular users will get a 404.
|
|
366
|
+
"""
|
|
367
|
+
response = self._client.get("/portfolio/summary/total_resting_order_value")
|
|
368
|
+
return response.get("total_resting_order_value", 0)
|
|
369
|
+
|
|
370
|
+
# --- Order Groups (OCO, Bracket Orders) ---
|
|
371
|
+
|
|
372
|
+
def create_order_group(
|
|
373
|
+
self,
|
|
374
|
+
order_ids: list[str],
|
|
375
|
+
*,
|
|
376
|
+
max_profit: int | None = None,
|
|
377
|
+
max_loss: int | None = None,
|
|
378
|
+
) -> OrderGroupModel:
|
|
379
|
+
"""Create an order group linking multiple orders.
|
|
380
|
+
|
|
381
|
+
When one order fills, the group can trigger cancellation or
|
|
382
|
+
execution of other orders based on the limit settings.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
order_ids: List of order IDs to link.
|
|
386
|
+
max_profit: Trigger when profit reaches this value (cents).
|
|
387
|
+
max_loss: Trigger when loss reaches this value (cents).
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Created OrderGroupModel.
|
|
391
|
+
"""
|
|
392
|
+
body: dict = {"order_ids": order_ids}
|
|
393
|
+
if max_profit is not None:
|
|
394
|
+
body["max_profit"] = max_profit
|
|
395
|
+
if max_loss is not None:
|
|
396
|
+
body["max_loss"] = max_loss
|
|
397
|
+
|
|
398
|
+
response = self._client.post("/portfolio/order_groups", body)
|
|
399
|
+
return OrderGroupModel.model_validate(response.get("order_group", response))
|
|
400
|
+
|
|
401
|
+
def get_order_group(self, order_group_id: str) -> OrderGroupModel:
|
|
402
|
+
"""Get an order group by ID."""
|
|
403
|
+
response = self._client.get(f"/portfolio/order_groups/{order_group_id}")
|
|
404
|
+
return OrderGroupModel.model_validate(response.get("order_group", response))
|
|
405
|
+
|
|
406
|
+
def trigger_order_group(self, order_group_id: str) -> OrderGroupModel:
|
|
407
|
+
"""Manually trigger an order group."""
|
|
408
|
+
response = self._client.post(f"/portfolio/order_groups/{order_group_id}/trigger", {})
|
|
409
|
+
return OrderGroupModel.model_validate(response.get("order_group", response))
|
|
410
|
+
|
|
411
|
+
def delete_order_group(self, order_group_id: str) -> None:
|
|
412
|
+
"""Delete an order group (does not cancel the orders)."""
|
|
413
|
+
self._client.delete(f"/portfolio/order_groups/{order_group_id}")
|
|
414
|
+
|
|
415
|
+
def get_order_groups(
|
|
416
|
+
self,
|
|
417
|
+
limit: int = 100,
|
|
418
|
+
cursor: str | None = None,
|
|
419
|
+
fetch_all: bool = False,
|
|
420
|
+
) -> list[OrderGroupModel]:
|
|
421
|
+
"""List all order groups.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
limit: Maximum results per page (default 100).
|
|
425
|
+
cursor: Pagination cursor for fetching next page.
|
|
426
|
+
fetch_all: If True, automatically fetch all pages.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
List of OrderGroupModel objects.
|
|
430
|
+
"""
|
|
431
|
+
params = {"limit": limit, "cursor": cursor}
|
|
432
|
+
data = self._client.paginated_get(
|
|
433
|
+
"/portfolio/order_groups", "order_groups", params, fetch_all
|
|
434
|
+
)
|
|
435
|
+
return [OrderGroupModel.model_validate(og) for og in data]
|
|
436
|
+
|
|
437
|
+
def reset_order_group(self, order_group_id: str) -> OrderGroupModel:
|
|
438
|
+
"""Reset matched contract counter for an order group.
|
|
439
|
+
|
|
440
|
+
Useful for reusing a bracket/OCO after partial fills.
|
|
441
|
+
"""
|
|
442
|
+
response = self._client.post(
|
|
443
|
+
f"/portfolio/order_groups/{order_group_id}/reset", {}
|
|
444
|
+
)
|
|
445
|
+
return OrderGroupModel.model_validate(response.get("order_group", response))
|
|
446
|
+
|
|
447
|
+
def update_order_group_limit(
|
|
448
|
+
self,
|
|
449
|
+
order_group_id: str,
|
|
450
|
+
*,
|
|
451
|
+
max_profit: int | None = None,
|
|
452
|
+
max_loss: int | None = None,
|
|
453
|
+
) -> OrderGroupModel:
|
|
454
|
+
"""Update the contract limit for an order group.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
order_group_id: ID of the order group.
|
|
458
|
+
max_profit: New max profit trigger (cents).
|
|
459
|
+
max_loss: New max loss trigger (cents).
|
|
460
|
+
"""
|
|
461
|
+
body: dict = {}
|
|
462
|
+
if max_profit is not None:
|
|
463
|
+
body["max_profit"] = max_profit
|
|
464
|
+
if max_loss is not None:
|
|
465
|
+
body["max_loss"] = max_loss
|
|
466
|
+
|
|
467
|
+
response = self._client.post(
|
|
468
|
+
f"/portfolio/order_groups/{order_group_id}/limit", body
|
|
469
|
+
)
|
|
470
|
+
return OrderGroupModel.model_validate(response.get("order_group", response))
|
|
471
|
+
|
|
472
|
+
# --- Subaccounts ---
|
|
473
|
+
|
|
474
|
+
def create_subaccount(self) -> SubaccountModel:
|
|
475
|
+
"""Create a new numbered subaccount.
|
|
476
|
+
|
|
477
|
+
Subaccounts allow strategy isolation - run multiple bots
|
|
478
|
+
with separate capital pools under one API key.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Created SubaccountModel with ID and number.
|
|
482
|
+
"""
|
|
483
|
+
response = self._client.post("/portfolio/subaccounts", {})
|
|
484
|
+
return SubaccountModel.model_validate(response.get("subaccount", response))
|
|
485
|
+
|
|
486
|
+
def transfer_between_subaccounts(
|
|
487
|
+
self,
|
|
488
|
+
from_subaccount_id: str,
|
|
489
|
+
to_subaccount_id: str,
|
|
490
|
+
amount: int,
|
|
491
|
+
) -> SubaccountTransferModel:
|
|
492
|
+
"""Transfer funds between subaccounts.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
from_subaccount_id: Source subaccount ID.
|
|
496
|
+
to_subaccount_id: Destination subaccount ID.
|
|
497
|
+
amount: Amount to transfer in cents.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Transfer record.
|
|
501
|
+
"""
|
|
502
|
+
body = {
|
|
503
|
+
"from_subaccount_id": from_subaccount_id,
|
|
504
|
+
"to_subaccount_id": to_subaccount_id,
|
|
505
|
+
"amount": amount,
|
|
506
|
+
}
|
|
507
|
+
response = self._client.post("/portfolio/subaccounts/transfer", body)
|
|
508
|
+
return SubaccountTransferModel.model_validate(response.get("transfer", response))
|
|
509
|
+
|
|
510
|
+
def get_subaccount_balances(self) -> list[SubaccountBalanceModel]:
|
|
511
|
+
"""Get balances for all subaccounts.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
List of SubaccountBalanceModel with balance per subaccount.
|
|
515
|
+
"""
|
|
516
|
+
response = self._client.get("/portfolio/subaccounts/balances")
|
|
517
|
+
return [
|
|
518
|
+
SubaccountBalanceModel.model_validate(b)
|
|
519
|
+
for b in response.get("balances", [])
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
def get_subaccount_transfers(
|
|
523
|
+
self,
|
|
524
|
+
limit: int = 100,
|
|
525
|
+
cursor: str | None = None,
|
|
526
|
+
fetch_all: bool = False,
|
|
527
|
+
) -> list[SubaccountTransferModel]:
|
|
528
|
+
"""Get transfer history between subaccounts.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
limit: Maximum results per page (default 100).
|
|
532
|
+
cursor: Pagination cursor for fetching next page.
|
|
533
|
+
fetch_all: If True, automatically fetch all pages.
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
List of transfer records.
|
|
537
|
+
"""
|
|
538
|
+
params = {"limit": limit, "cursor": cursor}
|
|
539
|
+
data = self._client.paginated_get(
|
|
540
|
+
"/portfolio/subaccounts/transfers", "transfers", params, fetch_all
|
|
541
|
+
)
|
|
542
|
+
return [SubaccountTransferModel.model_validate(t) for t in data]
|
kalshi_api/py.typed
ADDED
|
File without changes
|