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.
- pmxt/__init__.py +58 -0
- pmxt/client.py +713 -0
- pmxt/models.py +296 -0
- pmxt/server_manager.py +242 -0
- pmxt-1.0.0.dist-info/METADATA +250 -0
- pmxt-1.0.0.dist-info/RECORD +56 -0
- pmxt-1.0.0.dist-info/WHEEL +5 -0
- pmxt-1.0.0.dist-info/top_level.txt +2 -0
- pmxt_internal/__init__.py +124 -0
- pmxt_internal/api/__init__.py +5 -0
- pmxt_internal/api/default_api.py +3722 -0
- pmxt_internal/api_client.py +804 -0
- pmxt_internal/api_response.py +21 -0
- pmxt_internal/configuration.py +578 -0
- pmxt_internal/exceptions.py +219 -0
- pmxt_internal/models/__init__.py +54 -0
- pmxt_internal/models/balance.py +93 -0
- pmxt_internal/models/base_request.py +91 -0
- pmxt_internal/models/base_response.py +93 -0
- pmxt_internal/models/cancel_order_request.py +94 -0
- pmxt_internal/models/create_order200_response.py +99 -0
- pmxt_internal/models/create_order_params.py +111 -0
- pmxt_internal/models/create_order_request.py +102 -0
- pmxt_internal/models/error_detail.py +87 -0
- pmxt_internal/models/error_response.py +93 -0
- pmxt_internal/models/exchange_credentials.py +93 -0
- pmxt_internal/models/fetch_balance200_response.py +103 -0
- pmxt_internal/models/fetch_markets200_response.py +103 -0
- pmxt_internal/models/fetch_markets_request.py +102 -0
- pmxt_internal/models/fetch_ohlcv200_response.py +103 -0
- pmxt_internal/models/fetch_ohlcv_request.py +102 -0
- pmxt_internal/models/fetch_ohlcv_request_args_inner.py +140 -0
- pmxt_internal/models/fetch_open_orders200_response.py +103 -0
- pmxt_internal/models/fetch_open_orders_request.py +94 -0
- pmxt_internal/models/fetch_order_book200_response.py +99 -0
- pmxt_internal/models/fetch_order_book_request.py +94 -0
- pmxt_internal/models/fetch_positions200_response.py +103 -0
- pmxt_internal/models/fetch_positions_request.py +94 -0
- pmxt_internal/models/fetch_trades200_response.py +103 -0
- pmxt_internal/models/fetch_trades_request.py +102 -0
- pmxt_internal/models/get_markets_by_slug_request.py +94 -0
- pmxt_internal/models/health_check200_response.py +89 -0
- pmxt_internal/models/history_filter_params.py +101 -0
- pmxt_internal/models/market_filter_params.py +113 -0
- pmxt_internal/models/market_outcome.py +95 -0
- pmxt_internal/models/order.py +139 -0
- pmxt_internal/models/order_book.py +106 -0
- pmxt_internal/models/order_level.py +89 -0
- pmxt_internal/models/position.py +101 -0
- pmxt_internal/models/price_candle.py +97 -0
- pmxt_internal/models/search_markets_request.py +102 -0
- pmxt_internal/models/search_markets_request_args_inner.py +140 -0
- pmxt_internal/models/trade.py +105 -0
- pmxt_internal/models/unified_market.py +120 -0
- pmxt_internal/py.typed +0 -0
- 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
|
+
)
|