pmxt 1.0.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.
Files changed (56) hide show
  1. pmxt/__init__.py +58 -0
  2. pmxt/client.py +713 -0
  3. pmxt/models.py +296 -0
  4. pmxt/server_manager.py +242 -0
  5. pmxt-1.0.0.dist-info/METADATA +250 -0
  6. pmxt-1.0.0.dist-info/RECORD +56 -0
  7. pmxt-1.0.0.dist-info/WHEEL +5 -0
  8. pmxt-1.0.0.dist-info/top_level.txt +2 -0
  9. pmxt_internal/__init__.py +124 -0
  10. pmxt_internal/api/__init__.py +5 -0
  11. pmxt_internal/api/default_api.py +3722 -0
  12. pmxt_internal/api_client.py +804 -0
  13. pmxt_internal/api_response.py +21 -0
  14. pmxt_internal/configuration.py +578 -0
  15. pmxt_internal/exceptions.py +219 -0
  16. pmxt_internal/models/__init__.py +54 -0
  17. pmxt_internal/models/balance.py +93 -0
  18. pmxt_internal/models/base_request.py +91 -0
  19. pmxt_internal/models/base_response.py +93 -0
  20. pmxt_internal/models/cancel_order_request.py +94 -0
  21. pmxt_internal/models/create_order200_response.py +99 -0
  22. pmxt_internal/models/create_order_params.py +111 -0
  23. pmxt_internal/models/create_order_request.py +102 -0
  24. pmxt_internal/models/error_detail.py +87 -0
  25. pmxt_internal/models/error_response.py +93 -0
  26. pmxt_internal/models/exchange_credentials.py +93 -0
  27. pmxt_internal/models/fetch_balance200_response.py +103 -0
  28. pmxt_internal/models/fetch_markets200_response.py +103 -0
  29. pmxt_internal/models/fetch_markets_request.py +102 -0
  30. pmxt_internal/models/fetch_ohlcv200_response.py +103 -0
  31. pmxt_internal/models/fetch_ohlcv_request.py +102 -0
  32. pmxt_internal/models/fetch_ohlcv_request_args_inner.py +140 -0
  33. pmxt_internal/models/fetch_open_orders200_response.py +103 -0
  34. pmxt_internal/models/fetch_open_orders_request.py +94 -0
  35. pmxt_internal/models/fetch_order_book200_response.py +99 -0
  36. pmxt_internal/models/fetch_order_book_request.py +94 -0
  37. pmxt_internal/models/fetch_positions200_response.py +103 -0
  38. pmxt_internal/models/fetch_positions_request.py +94 -0
  39. pmxt_internal/models/fetch_trades200_response.py +103 -0
  40. pmxt_internal/models/fetch_trades_request.py +102 -0
  41. pmxt_internal/models/get_markets_by_slug_request.py +94 -0
  42. pmxt_internal/models/health_check200_response.py +89 -0
  43. pmxt_internal/models/history_filter_params.py +101 -0
  44. pmxt_internal/models/market_filter_params.py +113 -0
  45. pmxt_internal/models/market_outcome.py +95 -0
  46. pmxt_internal/models/order.py +139 -0
  47. pmxt_internal/models/order_book.py +106 -0
  48. pmxt_internal/models/order_level.py +89 -0
  49. pmxt_internal/models/position.py +101 -0
  50. pmxt_internal/models/price_candle.py +97 -0
  51. pmxt_internal/models/search_markets_request.py +102 -0
  52. pmxt_internal/models/search_markets_request_args_inner.py +140 -0
  53. pmxt_internal/models/trade.py +105 -0
  54. pmxt_internal/models/unified_market.py +120 -0
  55. pmxt_internal/py.typed +0 -0
  56. pmxt_internal/rest.py +263 -0
pmxt/client.py ADDED
@@ -0,0 +1,713 @@
1
+ """
2
+ Exchange client implementations.
3
+
4
+ This module provides clean, Pythonic wrappers around the auto-generated
5
+ OpenAPI client, matching the JavaScript API exactly.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from typing import List, Optional, Dict, Any
11
+ from datetime import datetime
12
+ from abc import ABC, abstractmethod
13
+
14
+ # Add generated client to path
15
+ _GENERATED_PATH = os.path.join(os.path.dirname(__file__), "..", "generated")
16
+ if _GENERATED_PATH not in sys.path:
17
+ sys.path.insert(0, _GENERATED_PATH)
18
+
19
+ from pmxt_internal import ApiClient, Configuration
20
+ from pmxt_internal.api.default_api import DefaultApi
21
+ from pmxt_internal.exceptions import ApiException
22
+ from pmxt_internal import models as internal_models
23
+
24
+ from .models import (
25
+ UnifiedMarket,
26
+ MarketOutcome,
27
+ PriceCandle,
28
+ OrderBook,
29
+ OrderLevel,
30
+ Trade,
31
+ Order,
32
+ Position,
33
+ Balance,
34
+ MarketFilterParams,
35
+ HistoryFilterParams,
36
+ CreateOrderParams,
37
+ )
38
+ from .server_manager import ServerManager
39
+
40
+
41
+ def _convert_market(raw: Dict[str, Any]) -> UnifiedMarket:
42
+ """Convert raw API response to UnifiedMarket."""
43
+ outcomes = [
44
+ MarketOutcome(
45
+ id=o.get("id"),
46
+ label=o.get("label"),
47
+ price=o.get("price"),
48
+ price_change_24h=o.get("priceChange24h"),
49
+ metadata=o.get("metadata"),
50
+ )
51
+ for o in raw.get("outcomes", [])
52
+ ]
53
+
54
+ return UnifiedMarket(
55
+ id=raw.get("id"),
56
+ title=raw.get("title"),
57
+ outcomes=outcomes,
58
+ volume_24h=raw.get("volume24h", 0),
59
+ liquidity=raw.get("liquidity", 0),
60
+ url=raw.get("url"),
61
+ description=raw.get("description"),
62
+ resolution_date=None, # TODO: Parse if present
63
+ volume=raw.get("volume"),
64
+ open_interest=raw.get("openInterest"),
65
+ image=raw.get("image"),
66
+ category=raw.get("category"),
67
+ tags=raw.get("tags"),
68
+ )
69
+
70
+
71
+ def _convert_candle(raw: Dict[str, Any]) -> PriceCandle:
72
+ """Convert raw API response to PriceCandle."""
73
+ return PriceCandle(
74
+ timestamp=raw.get("timestamp"),
75
+ open=raw.get("open"),
76
+ high=raw.get("high"),
77
+ low=raw.get("low"),
78
+ close=raw.get("close"),
79
+ volume=raw.get("volume"),
80
+ )
81
+
82
+
83
+ def _convert_order_book(raw: Dict[str, Any]) -> OrderBook:
84
+ """Convert raw API response to OrderBook."""
85
+ bids = [OrderLevel(price=b.get("price"), size=b.get("size")) for b in raw.get("bids", [])]
86
+ asks = [OrderLevel(price=a.get("price"), size=a.get("size")) for a in raw.get("asks", [])]
87
+
88
+ return OrderBook(
89
+ bids=bids,
90
+ asks=asks,
91
+ timestamp=raw.get("timestamp"),
92
+ )
93
+
94
+
95
+ def _convert_trade(raw: Dict[str, Any]) -> Trade:
96
+ """Convert raw API response to Trade."""
97
+ return Trade(
98
+ id=raw.get("id"),
99
+ timestamp=raw.get("timestamp"),
100
+ price=raw.get("price"),
101
+ amount=raw.get("amount"),
102
+ side=raw.get("side", "unknown"),
103
+ )
104
+
105
+
106
+ def _convert_order(raw: Dict[str, Any]) -> Order:
107
+ """Convert raw API response to Order."""
108
+ return Order(
109
+ id=raw.get("id"),
110
+ market_id=raw.get("marketId"),
111
+ outcome_id=raw.get("outcomeId"),
112
+ side=raw.get("side"),
113
+ type=raw.get("type"),
114
+ amount=raw.get("amount"),
115
+ status=raw.get("status"),
116
+ filled=raw.get("filled"),
117
+ remaining=raw.get("remaining"),
118
+ timestamp=raw.get("timestamp"),
119
+ price=raw.get("price"),
120
+ fee=raw.get("fee"),
121
+ )
122
+
123
+
124
+ def _convert_position(raw: Dict[str, Any]) -> Position:
125
+ """Convert raw API response to Position."""
126
+ return Position(
127
+ market_id=raw.get("marketId"),
128
+ outcome_id=raw.get("outcomeId"),
129
+ outcome_label=raw.get("outcomeLabel"),
130
+ size=raw.get("size"),
131
+ entry_price=raw.get("entryPrice"),
132
+ current_price=raw.get("currentPrice"),
133
+ unrealized_pnl=raw.get("unrealizedPnL"),
134
+ realized_pnl=raw.get("realizedPnL"),
135
+ )
136
+
137
+
138
+ def _convert_balance(raw: Dict[str, Any]) -> Balance:
139
+ """Convert raw API response to Balance."""
140
+ return Balance(
141
+ currency=raw.get("currency"),
142
+ total=raw.get("total"),
143
+ available=raw.get("available"),
144
+ locked=raw.get("locked"),
145
+ )
146
+
147
+
148
+ class Exchange(ABC):
149
+ """
150
+ Base class for prediction market exchanges.
151
+
152
+ This provides a unified interface for interacting with different
153
+ prediction market platforms (Polymarket, Kalshi, etc.).
154
+ """
155
+
156
+ def __init__(
157
+ self,
158
+ exchange_name: str,
159
+ api_key: Optional[str] = None,
160
+ private_key: Optional[str] = None,
161
+ base_url: str = "http://localhost:3847",
162
+ auto_start_server: bool = True,
163
+ ):
164
+ """
165
+ Initialize an exchange client.
166
+
167
+ Args:
168
+ exchange_name: Name of the exchange ("polymarket" or "kalshi")
169
+ api_key: API key for authentication (optional)
170
+ private_key: Private key for authentication (optional)
171
+ base_url: Base URL of the PMXT sidecar server
172
+ auto_start_server: Automatically start server if not running (default: True)
173
+ """
174
+ self.exchange_name = exchange_name.lower()
175
+ self.api_key = api_key
176
+ self.private_key = private_key
177
+
178
+ # Initialize server manager
179
+ self._server_manager = ServerManager(base_url)
180
+
181
+ # Ensure server is running (unless disabled)
182
+ if auto_start_server:
183
+ try:
184
+ self._server_manager.ensure_server_running()
185
+ except Exception as e:
186
+ raise Exception(
187
+ f"Failed to start PMXT server: {e}\n\n"
188
+ f"Please ensure 'pmxtjs' is installed: npm install -g pmxtjs\n"
189
+ f"Or start the server manually: pmxt-server"
190
+ )
191
+
192
+ # Configure the API client
193
+ config = Configuration(host=base_url)
194
+ self._api_client = ApiClient(configuration=config)
195
+ self._api = DefaultApi(api_client=self._api_client)
196
+
197
+ def _handle_response(self, response: Dict[str, Any]) -> Any:
198
+ """Handle API response and extract data."""
199
+ if not response.get("success"):
200
+ error = response.get("error", {})
201
+ raise Exception(error.get("message", "Unknown error"))
202
+ return response.get("data")
203
+
204
+ def _get_credentials_dict(self) -> Optional[Dict[str, Any]]:
205
+ """Build credentials dictionary for API requests."""
206
+ if not self.api_key and not self.private_key:
207
+ return None
208
+
209
+ creds = {}
210
+ if self.api_key:
211
+ creds["apiKey"] = self.api_key
212
+ if self.private_key:
213
+ creds["privateKey"] = self.private_key
214
+ return creds if creds else None
215
+
216
+ # Market Data Methods
217
+
218
+ def fetch_markets(self, params: Optional[MarketFilterParams] = None) -> List[UnifiedMarket]:
219
+ """
220
+ Get active markets from the exchange.
221
+
222
+ Args:
223
+ params: Optional filter parameters
224
+
225
+ Returns:
226
+ List of unified markets
227
+
228
+ Example:
229
+ >>> markets = exchange.fetch_markets(MarketFilterParams(limit=20, sort="volume"))
230
+ """
231
+ try:
232
+ body_dict = {"args": []}
233
+ if params:
234
+ body_dict["args"] = [params.__dict__]
235
+
236
+ # Add credentials if available
237
+ creds = self._get_credentials_dict()
238
+ if creds:
239
+ body_dict["credentials"] = creds
240
+
241
+ request_body = internal_models.FetchMarketsRequest.from_dict(body_dict)
242
+
243
+ response = self._api.fetch_markets(
244
+ exchange=self.exchange_name,
245
+ fetch_markets_request=request_body,
246
+ )
247
+
248
+ data = self._handle_response(response.to_dict())
249
+ return [_convert_market(m) for m in data]
250
+ except ApiException as e:
251
+ raise Exception(f"Failed to fetch markets: {e}")
252
+
253
+ def search_markets(
254
+ self,
255
+ query: str,
256
+ params: Optional[MarketFilterParams] = None,
257
+ ) -> List[UnifiedMarket]:
258
+ """
259
+ Search markets by keyword.
260
+
261
+ Args:
262
+ query: Search query
263
+ params: Optional filter parameters
264
+
265
+ Returns:
266
+ List of matching markets
267
+
268
+ Example:
269
+ >>> markets = exchange.search_markets("Trump", MarketFilterParams(limit=10))
270
+ """
271
+ try:
272
+ args = [query]
273
+ if params:
274
+ args.append(params.__dict__)
275
+
276
+ body_dict = {"args": args}
277
+ request_body = internal_models.SearchMarketsRequest.from_dict(body_dict)
278
+
279
+ response = self._api.search_markets(
280
+ exchange=self.exchange_name,
281
+ search_markets_request=request_body,
282
+ )
283
+
284
+ data = self._handle_response(response.to_dict())
285
+ return [_convert_market(m) for m in data]
286
+ except ApiException as e:
287
+ raise Exception(f"Failed to search markets: {e}")
288
+
289
+ def get_markets_by_slug(self, slug: str) -> List[UnifiedMarket]:
290
+ """
291
+ Fetch markets by URL slug/ticker.
292
+
293
+ Args:
294
+ slug: Market slug (Polymarket) or ticker (Kalshi)
295
+
296
+ Returns:
297
+ List of matching markets
298
+
299
+ Example:
300
+ >>> # Polymarket
301
+ >>> markets = poly.get_markets_by_slug("who-will-trump-nominate-as-fed-chair")
302
+ >>>
303
+ >>> # Kalshi
304
+ >>> markets = kalshi.get_markets_by_slug("KXFEDCHAIRNOM-29")
305
+ """
306
+ try:
307
+ body_dict = {"args": [slug]}
308
+ request_body = internal_models.GetMarketsBySlugRequest.from_dict(body_dict)
309
+
310
+ response = self._api.get_markets_by_slug(
311
+ exchange=self.exchange_name,
312
+ get_markets_by_slug_request=request_body,
313
+ )
314
+
315
+ data = self._handle_response(response.to_dict())
316
+ return [_convert_market(m) for m in data]
317
+ except ApiException as e:
318
+ raise Exception(f"Failed to get markets by slug: {e}")
319
+
320
+ def fetch_ohlcv(
321
+ self,
322
+ outcome_id: str,
323
+ params: HistoryFilterParams,
324
+ ) -> List[PriceCandle]:
325
+ """
326
+ Get historical price candles.
327
+
328
+ **CRITICAL**: Use outcome.id, not market.id.
329
+ - Polymarket: outcome.id is the CLOB Token ID
330
+ - Kalshi: outcome.id is the Market Ticker
331
+
332
+ Args:
333
+ outcome_id: Outcome ID (from market.outcomes[].id)
334
+ params: History filter parameters
335
+
336
+ Returns:
337
+ List of price candles
338
+
339
+ Example:
340
+ >>> markets = exchange.search_markets("Trump")
341
+ >>> outcome_id = markets[0].outcomes[0].id
342
+ >>> candles = exchange.fetch_ohlcv(
343
+ ... outcome_id,
344
+ ... HistoryFilterParams(resolution="1h", limit=100)
345
+ ... )
346
+ """
347
+ try:
348
+ params_dict = {"resolution": params.resolution}
349
+ if params.start:
350
+ params_dict["start"] = params.start.isoformat()
351
+ if params.end:
352
+ params_dict["end"] = params.end.isoformat()
353
+ if params.limit:
354
+ params_dict["limit"] = params.limit
355
+
356
+ request_body_dict = {"args": [outcome_id, params_dict]}
357
+ request_body = internal_models.FetchOHLCVRequest.from_dict(request_body_dict)
358
+
359
+ response = self._api.fetch_ohlcv(
360
+ exchange=self.exchange_name,
361
+ fetch_ohlcv_request=request_body,
362
+ )
363
+
364
+ data = self._handle_response(response.to_dict())
365
+ return [_convert_candle(c) for c in data]
366
+ except ApiException as e:
367
+ raise Exception(f"Failed to fetch OHLCV: {e}")
368
+
369
+ def fetch_order_book(self, outcome_id: str) -> OrderBook:
370
+ """
371
+ Get current order book for an outcome.
372
+
373
+ Args:
374
+ outcome_id: Outcome ID
375
+
376
+ Returns:
377
+ Current order book
378
+
379
+ Example:
380
+ >>> order_book = exchange.fetch_order_book(outcome_id)
381
+ >>> print(f"Best bid: {order_book.bids[0].price}")
382
+ >>> print(f"Best ask: {order_book.asks[0].price}")
383
+ """
384
+ try:
385
+ body_dict = {"args": [outcome_id]}
386
+ request_body = internal_models.FetchOrderBookRequest.from_dict(body_dict)
387
+
388
+ response = self._api.fetch_order_book(
389
+ exchange=self.exchange_name,
390
+ fetch_order_book_request=request_body,
391
+ )
392
+
393
+ data = self._handle_response(response.to_dict())
394
+ return _convert_order_book(data)
395
+ except ApiException as e:
396
+ raise Exception(f"Failed to fetch order book: {e}")
397
+
398
+ def fetch_trades(
399
+ self,
400
+ outcome_id: str,
401
+ params: HistoryFilterParams,
402
+ ) -> List[Trade]:
403
+ """
404
+ Get trade history for an outcome.
405
+
406
+ Note: Polymarket requires API key.
407
+
408
+ Args:
409
+ outcome_id: Outcome ID
410
+ params: History filter parameters
411
+
412
+ Returns:
413
+ List of trades
414
+ """
415
+ try:
416
+ params_dict = {"resolution": params.resolution}
417
+ if params.limit:
418
+ params_dict["limit"] = params.limit
419
+
420
+ request_body_dict = {"args": [outcome_id, params_dict]}
421
+ request_body = internal_models.FetchTradesRequest.from_dict(request_body_dict)
422
+
423
+ response = self._api.fetch_trades(
424
+ exchange=self.exchange_name,
425
+ fetch_trades_request=request_body,
426
+ )
427
+
428
+ data = self._handle_response(response.to_dict())
429
+ return [_convert_trade(t) for t in data]
430
+ except ApiException as e:
431
+ raise Exception(f"Failed to fetch trades: {e}")
432
+
433
+ # Trading Methods (require authentication)
434
+
435
+ def create_order(self, params: CreateOrderParams) -> Order:
436
+ """
437
+ Create a new order.
438
+
439
+ Args:
440
+ params: Order parameters
441
+
442
+ Returns:
443
+ Created order
444
+
445
+ Example:
446
+ >>> order = exchange.create_order(CreateOrderParams(
447
+ ... market_id="663583",
448
+ ... outcome_id="10991849...",
449
+ ... side="buy",
450
+ ... type="limit",
451
+ ... amount=10,
452
+ ... price=0.55
453
+ ... ))
454
+ """
455
+ try:
456
+ params_dict = {
457
+ "marketId": params.market_id,
458
+ "outcomeId": params.outcome_id,
459
+ "side": params.side,
460
+ "type": params.type,
461
+ "amount": params.amount,
462
+ }
463
+ if params.price is not None:
464
+ params_dict["price"] = params.price
465
+
466
+ request_body_dict = {"args": [params_dict]}
467
+
468
+ # Add credentials if available
469
+ creds = self._get_credentials_dict()
470
+ if creds:
471
+ request_body_dict["credentials"] = creds
472
+
473
+ request_body = internal_models.CreateOrderRequest.from_dict(request_body_dict)
474
+
475
+ response = self._api.create_order(
476
+ exchange=self.exchange_name,
477
+ create_order_request=request_body,
478
+ )
479
+
480
+ data = self._handle_response(response.to_dict())
481
+ return _convert_order(data)
482
+ except ApiException as e:
483
+ raise Exception(f"Failed to create order: {e}")
484
+
485
+ def cancel_order(self, order_id: str) -> Order:
486
+ """
487
+ Cancel an open order.
488
+
489
+ Args:
490
+ order_id: Order ID to cancel
491
+
492
+ Returns:
493
+ Cancelled order
494
+ """
495
+ try:
496
+ body_dict = {"args": [order_id]}
497
+
498
+ # Add credentials if available
499
+ creds = self._get_credentials_dict()
500
+ if creds:
501
+ body_dict["credentials"] = creds
502
+
503
+ request_body = internal_models.CancelOrderRequest.from_dict(body_dict)
504
+
505
+ response = self._api.cancel_order(
506
+ exchange=self.exchange_name,
507
+ cancel_order_request=request_body,
508
+ )
509
+
510
+ data = self._handle_response(response.to_dict())
511
+ return _convert_order(data)
512
+ except ApiException as e:
513
+ raise Exception(f"Failed to cancel order: {e}")
514
+
515
+ def fetch_order(self, order_id: str) -> Order:
516
+ """
517
+ Get details of a specific order.
518
+
519
+ Args:
520
+ order_id: Order ID
521
+
522
+ Returns:
523
+ Order details
524
+ """
525
+ try:
526
+ body_dict = {"args": [order_id]}
527
+
528
+ # Add credentials if available
529
+ creds = self._get_credentials_dict()
530
+ if creds:
531
+ body_dict["credentials"] = creds
532
+
533
+ request_body = internal_models.FetchOrderRequest.from_dict(body_dict)
534
+
535
+ response = self._api.fetch_order(
536
+ exchange=self.exchange_name,
537
+ fetch_order_request=request_body,
538
+ )
539
+
540
+ data = self._handle_response(response.to_dict())
541
+ return _convert_order(data)
542
+ except ApiException as e:
543
+ raise Exception(f"Failed to fetch order: {e}")
544
+
545
+ def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]:
546
+ """
547
+ Get all open orders, optionally filtered by market.
548
+
549
+ Args:
550
+ market_id: Optional market ID to filter by
551
+
552
+ Returns:
553
+ List of open orders
554
+ """
555
+ try:
556
+ args = []
557
+ if market_id:
558
+ args.append(market_id)
559
+
560
+ body_dict = {"args": args}
561
+
562
+ # Add credentials if available
563
+ creds = self._get_credentials_dict()
564
+ if creds:
565
+ body_dict["credentials"] = creds
566
+
567
+ request_body = internal_models.FetchOpenOrdersRequest.from_dict(body_dict)
568
+
569
+ response = self._api.fetch_open_orders(
570
+ exchange=self.exchange_name,
571
+ fetch_open_orders_request=request_body,
572
+ )
573
+
574
+ data = self._handle_response(response.to_dict())
575
+ return [_convert_order(o) for o in data]
576
+ except ApiException as e:
577
+ raise Exception(f"Failed to fetch open orders: {e}")
578
+
579
+ # Account Methods
580
+
581
+ def fetch_positions(self) -> List[Position]:
582
+ """
583
+ Get current positions across all markets.
584
+
585
+ Returns:
586
+ List of positions
587
+ """
588
+ try:
589
+ body_dict = {"args": []}
590
+
591
+ # Add credentials if available
592
+ creds = self._get_credentials_dict()
593
+ if creds:
594
+ body_dict["credentials"] = creds
595
+
596
+ request_body = internal_models.FetchPositionsRequest.from_dict(body_dict)
597
+
598
+ response = self._api.fetch_positions(
599
+ exchange=self.exchange_name,
600
+ fetch_positions_request=request_body,
601
+ )
602
+
603
+ data = self._handle_response(response.to_dict())
604
+ return [_convert_position(p) for p in data]
605
+ except ApiException as e:
606
+ raise Exception(f"Failed to fetch positions: {e}")
607
+
608
+ def fetch_balance(self) -> List[Balance]:
609
+ """
610
+ Get account balance.
611
+
612
+ Returns:
613
+ List of balances (by currency)
614
+ """
615
+ try:
616
+ body_dict = {"args": []}
617
+
618
+ # Add credentials if available
619
+ creds = self._get_credentials_dict()
620
+ if creds:
621
+ body_dict["credentials"] = creds
622
+
623
+ # Note: Generator name for this request might be reused from FetchPositionsRequest
624
+ # if the schemas are identical (empty args array)
625
+ request_body = internal_models.FetchPositionsRequest.from_dict(body_dict)
626
+
627
+ response = self._api.fetch_balance(
628
+ exchange=self.exchange_name,
629
+ fetch_positions_request=request_body,
630
+ )
631
+
632
+ data = self._handle_response(response.to_dict())
633
+ return [_convert_balance(b) for b in data]
634
+ except ApiException as e:
635
+ raise Exception(f"Failed to fetch balance: {e}")
636
+
637
+
638
+ class Polymarket(Exchange):
639
+ """
640
+ Polymarket exchange client.
641
+
642
+ Example:
643
+ >>> # Public data (no auth)
644
+ >>> poly = Polymarket()
645
+ >>> markets = poly.search_markets("Trump")
646
+ >>>
647
+ >>> # Trading (requires auth)
648
+ >>> poly = Polymarket(private_key=os.getenv("POLYMARKET_PRIVATE_KEY"))
649
+ >>> balance = poly.fetch_balance()
650
+ """
651
+
652
+ def __init__(
653
+ self,
654
+ private_key: Optional[str] = None,
655
+ base_url: str = "http://localhost:3847",
656
+ auto_start_server: bool = True,
657
+ ):
658
+ """
659
+ Initialize Polymarket client.
660
+
661
+ Args:
662
+ private_key: Polygon private key (required for trading)
663
+ base_url: Base URL of the PMXT sidecar server
664
+ auto_start_server: Automatically start server if not running (default: True)
665
+ """
666
+ super().__init__(
667
+ exchange_name="polymarket",
668
+ private_key=private_key,
669
+ base_url=base_url,
670
+ auto_start_server=auto_start_server,
671
+ )
672
+
673
+
674
+ class Kalshi(Exchange):
675
+ """
676
+ Kalshi exchange client.
677
+
678
+ Example:
679
+ >>> # Public data (no auth)
680
+ >>> kalshi = Kalshi()
681
+ >>> markets = kalshi.search_markets("Fed rates")
682
+ >>>
683
+ >>> # Trading (requires auth)
684
+ >>> kalshi = Kalshi(
685
+ ... api_key=os.getenv("KALSHI_API_KEY"),
686
+ ... private_key=os.getenv("KALSHI_PRIVATE_KEY")
687
+ ... )
688
+ >>> balance = kalshi.fetch_balance()
689
+ """
690
+
691
+ def __init__(
692
+ self,
693
+ api_key: Optional[str] = None,
694
+ private_key: Optional[str] = None,
695
+ base_url: str = "http://localhost:3847",
696
+ auto_start_server: bool = True,
697
+ ):
698
+ """
699
+ Initialize Kalshi client.
700
+
701
+ Args:
702
+ api_key: Kalshi API key (required for trading)
703
+ private_key: Kalshi private key (required for trading)
704
+ base_url: Base URL of the PMXT sidecar server
705
+ auto_start_server: Automatically start server if not running (default: True)
706
+ """
707
+ super().__init__(
708
+ exchange_name="kalshi",
709
+ api_key=api_key,
710
+ private_key=private_key,
711
+ base_url=base_url,
712
+ auto_start_server=auto_start_server,
713
+ )