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/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}>"
@@ -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