hermes-pmxt 0.3.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.
@@ -0,0 +1,101 @@
1
+ """
2
+ hermes-pmxt -- Prediction market integration for Hermes Agent.
3
+
4
+ Usage:
5
+ from hermes_pmxt import pmxt_search, pmxt_quote, pmxt_order, ...
6
+
7
+ The package imports without pmxt installed. Tools that require pmxt
8
+ will raise ImportError with a helpful message at call time.
9
+ """
10
+
11
+ from hermes_pmxt.config import (
12
+ get_mode,
13
+ get_base_url,
14
+ runtime_status,
15
+ runtime_status_str,
16
+ )
17
+ from hermes_pmxt.exchanges import (
18
+ is_pmxt_available,
19
+ )
20
+ from hermes_pmxt.registry import (
21
+ TOOLS as PMXT_TOOLS,
22
+ KNOWN_EXCHANGES,
23
+ get_tool,
24
+ list_tools,
25
+ is_destructive,
26
+ requires_credentials,
27
+ )
28
+ from hermes_pmxt.shaper import (
29
+ shape_result,
30
+ )
31
+
32
+ from hermes_pmxt.tools import (
33
+ pmxt_arbitrage_scan,
34
+ pmxt_balance,
35
+ pmxt_build_order,
36
+ pmxt_call,
37
+ pmxt_cancel_order,
38
+ pmxt_compare_market,
39
+ pmxt_events,
40
+ pmxt_execution_price,
41
+ pmxt_list_exchanges,
42
+ pmxt_ohlcv,
43
+ pmxt_order,
44
+ pmxt_order_book,
45
+ pmxt_portfolio,
46
+ pmxt_positions,
47
+ pmxt_quote,
48
+ pmxt_runtime_status,
49
+ pmxt_search,
50
+ pmxt_server_health,
51
+ pmxt_server_start,
52
+ pmxt_server_status,
53
+ pmxt_server_stop,
54
+ pmxt_submit_order,
55
+ pmxt_trades,
56
+ )
57
+
58
+ __version__ = "0.3.0"
59
+
60
+ __all__ = [
61
+ # Config
62
+ "get_mode",
63
+ "get_base_url",
64
+ "runtime_status",
65
+ "runtime_status_str",
66
+ # Exchange
67
+ "is_pmxt_available",
68
+ "pmxt_list_exchanges",
69
+ "pmxt_runtime_status",
70
+ # Registry
71
+ "PMXT_TOOLS",
72
+ "KNOWN_EXCHANGES",
73
+ "get_tool",
74
+ "list_tools",
75
+ "is_destructive",
76
+ "requires_credentials",
77
+ # Shaper
78
+ "shape_result",
79
+ # Tools
80
+ "pmxt_call",
81
+ "pmxt_search",
82
+ "pmxt_quote",
83
+ "pmxt_order_book",
84
+ "pmxt_ohlcv",
85
+ "pmxt_trades",
86
+ "pmxt_events",
87
+ "pmxt_execution_price",
88
+ "pmxt_compare_market",
89
+ "pmxt_balance",
90
+ "pmxt_positions",
91
+ "pmxt_portfolio",
92
+ "pmxt_order",
93
+ "pmxt_build_order",
94
+ "pmxt_submit_order",
95
+ "pmxt_cancel_order",
96
+ "pmxt_arbitrage_scan",
97
+ "pmxt_server_health",
98
+ "pmxt_server_status",
99
+ "pmxt_server_start",
100
+ "pmxt_server_stop",
101
+ ]
hermes_pmxt/config.py ADDED
@@ -0,0 +1,145 @@
1
+ """
2
+ Runtime configuration and mode detection for hermes-pmxt.
3
+
4
+ Handles:
5
+ - PMXT_API_KEY, PMXT_API_URL, PMXT_BASE_URL, PMXT_WALLET_ADDRESS, PMXT_PRIVATE_KEY
6
+ - Hosted vs local sidecar vs custom server detection
7
+ - Capability reporting
8
+
9
+ All functions work without pmxt installed.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import sys
16
+ from typing import Optional
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Environment variable sources (ordered by precedence)
20
+ # ---------------------------------------------------------------------------
21
+
22
+ _HOSTED_URL = "https://api.pmxt.dev"
23
+ _TRADE_URL = "https://trade.pmxt.dev"
24
+ _LOCAL_URL = "http://localhost:3847"
25
+
26
+
27
+ def _get_api_key() -> Optional[str]:
28
+ """Return the PMXT API key if configured."""
29
+ return os.getenv("PMXT_API_KEY") or None
30
+
31
+
32
+ def _get_api_url() -> Optional[str]:
33
+ """Return the explicit API URL override."""
34
+ return os.getenv("PMXT_API_URL") or os.getenv("PMXT_BASE_URL") or None
35
+
36
+
37
+ def get_mode() -> str:
38
+ """
39
+ Detect the PMXT runtime mode.
40
+
41
+ Returns one of:
42
+ - 'hosted': PMXT_API_KEY is set, talks to api.pmxt.dev (or custom URL)
43
+ - 'custom': PMXT_API_URL / PMXT_BASE_URL is set, custom server
44
+ - 'local-sidecar': no key and no URL, assumes localhost:3847
45
+ - 'unconfigured': not even local mode (no sidecar detectable)
46
+ """
47
+ key = _get_api_key()
48
+ url = _get_api_url()
49
+
50
+ if url:
51
+ return "custom"
52
+ if key:
53
+ return "hosted"
54
+ return "local-sidecar"
55
+
56
+
57
+ def get_base_url() -> str:
58
+ """Return the base URL the SDK should use."""
59
+ url = _get_api_url()
60
+ if url:
61
+ return url.rstrip("/")
62
+ if _get_api_key():
63
+ return _HOSTED_URL
64
+ return _LOCAL_URL
65
+
66
+
67
+ def get_trade_url() -> str:
68
+ """Return the hosted trade URL (for writes/account state)."""
69
+ url = os.getenv("PMXT_TRADE_URL")
70
+ if url:
71
+ return url.rstrip("/")
72
+ return _TRADE_URL
73
+
74
+
75
+ def get_wallet_address() -> Optional[str]:
76
+ """Return the configured wallet address for hosted trading."""
77
+ return os.getenv("PMXT_WALLET_ADDRESS") or None
78
+
79
+
80
+ def get_private_key() -> Optional[str]:
81
+ """Return the configured private key for hosted trading."""
82
+ return os.getenv("PMXT_PRIVATE_KEY") or None
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Runtime status
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ def runtime_status() -> dict:
91
+ """
92
+ Return a comprehensive runtime status dict.
93
+
94
+ Works without pmxt installed. When pmxt is available, adds
95
+ version, sidecar status, and exchange availability info.
96
+ """
97
+ result: dict = {
98
+ "mode": get_mode(),
99
+ "base_url": get_base_url(),
100
+ "has_api_key": _get_api_key() is not None,
101
+ "has_wallet_address": get_wallet_address() is not None,
102
+ "has_private_key": get_private_key() is not None,
103
+ "pmxt_installed": False,
104
+ "pmxt_version": None,
105
+ "python_version": sys.version,
106
+ }
107
+
108
+ try:
109
+ from hermes_pmxt.exchanges import is_pmxt_available as _available
110
+
111
+ if _available():
112
+ import importlib.metadata as _metadata
113
+ import pmxt # type: ignore[import-untyped]
114
+
115
+ result["pmxt_installed"] = True
116
+ result["pmxt_version"] = getattr(pmxt, "__version__", None) or _metadata.version("pmxt")
117
+
118
+ # Sidecar status (best effort)
119
+ try:
120
+ s = pmxt.server.status()
121
+ if isinstance(s, dict):
122
+ result["sidecar_running"] = s.get("running", False)
123
+ result["sidecar_port"] = s.get("port")
124
+ result["sidecar_pid"] = s.get("pid")
125
+ except Exception:
126
+ result["sidecar_running"] = False
127
+ except ImportError:
128
+ pass
129
+
130
+ return result
131
+
132
+
133
+ def runtime_status_str() -> str:
134
+ """Return a human-readable one-liner of runtime status."""
135
+ status = runtime_status()
136
+ parts = [f"mode={status['mode']}", f"url={status['base_url']}"]
137
+ if status["pmxt_installed"]:
138
+ parts.append(f"pmxt={status['pmxt_version'] or '?'}")
139
+ if status.get("sidecar_running"):
140
+ parts.append(f"sidecar=:{status.get('sidecar_port','?')}")
141
+ else:
142
+ parts.append("sidecar=off")
143
+ else:
144
+ parts.append("pmxt=not_installed")
145
+ return " ".join(parts)
@@ -0,0 +1,187 @@
1
+ """Exchange initialization, normalization, and sidecar helpers for pmxt."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from typing import Optional
8
+
9
+ _pmxt_module = None
10
+ _pmxt_import_error: Optional[str] = None
11
+
12
+
13
+ def _get_pmxt():
14
+ """Lazy-import pmxt. Raises ImportError only when pmxt functionality is actually used."""
15
+ global _pmxt_module, _pmxt_import_error
16
+ if _pmxt_module is not None:
17
+ return _pmxt_module
18
+ if _pmxt_import_error is not None:
19
+ raise ImportError(_pmxt_import_error)
20
+ try:
21
+ import pmxt as _mod
22
+
23
+ _pmxt_module = _mod
24
+ return _pmxt_module
25
+ except ImportError as exc:
26
+ _pmxt_import_error = (
27
+ "pmxt is not installed. Run: pip install pmxt. "
28
+ "Depending on your pmxt version, you may also need a pmxtjs sidecar."
29
+ )
30
+ raise ImportError(_pmxt_import_error) from exc
31
+
32
+
33
+ def is_pmxt_available() -> bool:
34
+ """Return True if pmxt can be imported, False otherwise."""
35
+ try:
36
+ _get_pmxt()
37
+ return True
38
+ except ImportError:
39
+ return False
40
+
41
+
42
+ _exchange_cache: dict[str, object] = {}
43
+
44
+ EXCHANGES = (
45
+ "polymarket",
46
+ "polymarket_us",
47
+ "kalshi",
48
+ "limitless",
49
+ "myriad",
50
+ "opinion",
51
+ "metaculus",
52
+ "smarkets",
53
+ )
54
+
55
+ TRADING_EXCHANGES = (
56
+ "polymarket",
57
+ "polymarket_us",
58
+ "kalshi",
59
+ "limitless",
60
+ )
61
+
62
+ _ALIASES = {
63
+ "polymarket-us": "polymarket_us",
64
+ "polymarket us": "polymarket_us",
65
+ "polymarketus": "polymarket_us",
66
+ }
67
+
68
+
69
+ def normalize_exchange_name(name: str) -> str:
70
+ """Normalize a user-facing exchange name to the package's canonical form."""
71
+ normalized = name.lower().strip().replace("-", "_")
72
+ normalized = _ALIASES.get(normalized, normalized)
73
+ return normalized.replace(" ", "_")
74
+
75
+
76
+ def _exchange_class(name: str):
77
+ """Resolve the pmxt exchange class for a normalized exchange name."""
78
+ normalized = normalize_exchange_name(name)
79
+ class_name = "".join(part.capitalize() for part in normalized.split("_"))
80
+ pmxt = _get_pmxt()
81
+ return getattr(pmxt, class_name, None)
82
+
83
+
84
+ def available_exchange_names() -> list[str]:
85
+ """Return exchange names that appear to exist in the installed pmxt build."""
86
+ return [name for name in EXCHANGES if _exchange_class(name) is not None]
87
+
88
+
89
+ def ensure_server() -> tuple[bool, Optional[str]]:
90
+ """Ensure the pmxt sidecar server is running. Returns (ok, error_msg)."""
91
+ pmxt = _get_pmxt()
92
+ try:
93
+ if not pmxt.server.health():
94
+ pmxt.server.start()
95
+ time.sleep(1.5)
96
+ if not pmxt.server.health():
97
+ return False, "Sidecar server failed to start"
98
+ return True, None
99
+ except Exception as e:
100
+ return False, f"Sidecar server error: {e}"
101
+
102
+
103
+ def get_exchange(name: str) -> tuple[Optional[object], Optional[str]]:
104
+ """
105
+ Get or initialize an exchange. Cached after first call.
106
+ Returns (exchange_instance, error_msg).
107
+ """
108
+ normalized = normalize_exchange_name(name)
109
+
110
+ if normalized in _exchange_cache:
111
+ return _exchange_cache[normalized], None
112
+
113
+ try:
114
+ exchange = _create_exchange(normalized)
115
+ _exchange_cache[normalized] = exchange
116
+ return exchange, None
117
+ except Exception as e:
118
+ return None, f"Failed to initialize {normalized}: {e}"
119
+
120
+
121
+ def _create_exchange(name: str):
122
+ """Instantiate an exchange by normalized name."""
123
+ normalized = normalize_exchange_name(name)
124
+ pmxt = _get_pmxt()
125
+
126
+ if normalized == "polymarket":
127
+ return pmxt.Polymarket(
128
+ private_key=os.getenv("POLYMARKET_PRIVATE_KEY"),
129
+ proxy_address=os.getenv("POLYMARKET_PROXY_ADDRESS"),
130
+ signature_type="gnosis-safe",
131
+ )
132
+ if normalized == "polymarket_us":
133
+ cls = _exchange_class(normalized)
134
+ if cls is None:
135
+ raise ValueError("Exchange polymarket_us is not available in this pmxt version")
136
+ return cls(
137
+ api_key=os.getenv("POLYMARKET_US_API_KEY"),
138
+ private_key=os.getenv("POLYMARKET_US_PRIVATE_KEY"),
139
+ )
140
+ if normalized == "kalshi":
141
+ return pmxt.Kalshi(
142
+ api_key=os.getenv("KALSHI_API_KEY"),
143
+ private_key=os.getenv("KALSHI_PRIVATE_KEY"),
144
+ )
145
+ if normalized == "limitless":
146
+ return pmxt.Limitless(
147
+ api_key=os.getenv("LIMITLESS_API_KEY"),
148
+ private_key=os.getenv("LIMITLESS_PRIVATE_KEY"),
149
+ )
150
+
151
+ cls = _exchange_class(normalized)
152
+ if cls is None:
153
+ raise ValueError(f"Unknown exchange: {normalized}")
154
+ return cls()
155
+
156
+
157
+ def server_status() -> dict:
158
+ """Get sidecar server status."""
159
+ pmxt = _get_pmxt()
160
+ try:
161
+ status = pmxt.server.status()
162
+ if isinstance(status, dict):
163
+ return {
164
+ "running": status.get("running", False),
165
+ "pid": status.get("pid"),
166
+ "port": status.get("port"),
167
+ "version": status.get("version"),
168
+ "uptime_seconds": status.get("uptimeSeconds") or status.get("uptime_seconds"),
169
+ }
170
+ return {
171
+ "running": getattr(status, "running", False),
172
+ "pid": getattr(status, "pid", None),
173
+ "port": getattr(status, "port", None),
174
+ "version": getattr(status, "version", None),
175
+ "uptime_seconds": getattr(status, "uptimeSeconds", None),
176
+ }
177
+ except Exception as e:
178
+ return {"running": False, "error": str(e)}
179
+
180
+
181
+ def server_logs(n: int = 50) -> list[str]:
182
+ """Get last N lines of server logs."""
183
+ pmxt = _get_pmxt()
184
+ try:
185
+ return list(pmxt.server.logs(n))
186
+ except Exception:
187
+ return []
@@ -0,0 +1,251 @@
1
+ """
2
+ Generated tool registry for PMXT methods.
3
+
4
+ Maps PMXT API methods to Python-friendly names with:
5
+ - arg specs for positional reconstruction
6
+ - safety annotations (read_only, destructive, idempotent)
7
+ - credential requirements
8
+ - supported exchanges
9
+
10
+ This file serves as a static snapshot. For auto-generation from PMXT
11
+ OpenAPI specs, run: python scripts/sync_pmxt_registry.py
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Optional
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Arg spec
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ class ArgSpec:
24
+ """Describes a single positional argument for a PMXT API method."""
25
+
26
+ __slots__ = ("name", "kind", "optional", "flatten")
27
+
28
+ def __init__(self, name: str, kind: str = "object", optional: bool = True, flatten: bool = False):
29
+ self.name = name
30
+ self.kind = kind
31
+ self.optional = optional
32
+ self.flatten = flatten
33
+
34
+ def to_dict(self) -> dict:
35
+ return {
36
+ "name": self.name,
37
+ "kind": self.kind,
38
+ "optional": self.optional,
39
+ "flatten": self.flatten,
40
+ }
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Tool definition
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ class ToolDef:
49
+ """Definition of a single PMXT tool/method."""
50
+
51
+ __slots__ = ("name", "method", "description", "args", "read_only", "destructive",
52
+ "idempotent", "requires_credentials", "category")
53
+
54
+ def __init__(
55
+ self,
56
+ name: str,
57
+ method: str,
58
+ description: str,
59
+ args: Optional[list[ArgSpec]] = None,
60
+ read_only: bool = True,
61
+ destructive: bool = False,
62
+ idempotent: bool = False,
63
+ requires_credentials: bool = False,
64
+ category: str = "data",
65
+ ):
66
+ self.name = name
67
+ self.method = method
68
+ self.description = description
69
+ self.args = args or []
70
+ self.read_only = read_only
71
+ self.destructive = destructive
72
+ self.idempotent = idempotent
73
+ self.requires_credentials = requires_credentials
74
+ self.category = category
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Supported exchanges (from pmxt-mcp generated tools.ts, v2.50.x)
79
+ # ---------------------------------------------------------------------------
80
+
81
+ KNOWN_EXCHANGES = [
82
+ "polymarket",
83
+ "polymarket_us",
84
+ "kalshi",
85
+ "kalshi-demo",
86
+ "limitless",
87
+ "probable",
88
+ "baozi",
89
+ "myriad",
90
+ "opinion",
91
+ "metaculus",
92
+ "smarkets",
93
+ "gemini-titan",
94
+ "hyperliquid",
95
+ "suibets",
96
+ "rain",
97
+ "mock",
98
+ "router",
99
+ ]
100
+
101
+ # Aliases for user-facing exchange names
102
+ EXCHANGE_ALIASES: dict[str, str] = {
103
+ "polymarket-us": "polymarket_us",
104
+ "polymarket us": "polymarket_us",
105
+ "polymarketus": "polymarket_us",
106
+ "kalshi-demo": "kalshi-demo",
107
+ "kalshi demo": "kalshi-demo",
108
+ "gemini-titan": "gemini-titan",
109
+ "gemini titan": "gemini-titan",
110
+ }
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Tool registry (static snapshot)
115
+ # ---------------------------------------------------------------------------
116
+
117
+
118
+ def _a(name: str, kind: str = "object", optional: bool = True, flatten: bool = False) -> ArgSpec:
119
+ return ArgSpec(name, kind, optional, flatten)
120
+
121
+
122
+ TOOLS: list[ToolDef] = [
123
+ # --- Market & Event Data ---
124
+ ToolDef("fetchMarkets", "fetchMarkets", "Search tradeable markets by query/slug/category.",
125
+ [_a("params", flatten=True)], read_only=True, category="data"),
126
+ ToolDef("fetchMarketsPaginated", "fetchMarketsPaginated",
127
+ "Paginated market fetch with cursor snapshot. First call w/o cursor fetches all; subsequent cursor calls slice from cache.",
128
+ [_a("params", flatten=True)], read_only=True, category="data"),
129
+ ToolDef("fetchEvents", "fetchEvents", "Search event groups (broad topics) containing child markets.",
130
+ [_a("params", flatten=True)], read_only=True, category="data"),
131
+ ToolDef("fetchEventsPaginated", "fetchEventsPaginated",
132
+ "Paginated event fetch with cursor snapshot.",
133
+ [_a("params", flatten=True)], read_only=True, category="data"),
134
+ ToolDef("fetchMarket", "fetchMarket", "Fetch a single market by ID, slug, or URL.",
135
+ [_a("params", flatten=True)], read_only=True, category="data"),
136
+ ToolDef("fetchEvent", "fetchEvent", "Fetch a single event by ID or slug.",
137
+ [_a("params", flatten=True)], read_only=True, category="data"),
138
+ ToolDef("fetchSeries", "fetchSeries", "Fetch series (4th tier below event/market/outcome).",
139
+ [_a("params", flatten=True)], read_only=True, category="data"),
140
+ ToolDef("loadMarkets", "loadMarkets", "Load and cache all markets locally. Recommended for stable iteration.",
141
+ [], read_only=True, idempotent=True, category="data"),
142
+
143
+ # --- Order Book & Pricing ---
144
+ ToolDef("fetchOHLCV", "fetchOHLCV", "Get price history candles for an outcome.",
145
+ [_a("outcomeId", "string", optional=False),
146
+ _a("params", flatten=True)], read_only=True, category="data"),
147
+ ToolDef("fetchOrderBook", "fetchOrderBook", "Get current order book depth for an outcome.",
148
+ [_a("outcomeId", "string", optional=False),
149
+ _a("limit", "number"),
150
+ _a("params", flatten=True)], read_only=True, category="data"),
151
+ ToolDef("fetchOrderBooks", "fetchOrderBooks", "Batch fetch order books for multiple outcome IDs.",
152
+ [_a("outcomeIds", "unknown", optional=False)], read_only=True, category="data"),
153
+ ToolDef("fetchTrades", "fetchTrades", "Get recent trades for an outcome.",
154
+ [_a("outcomeId", "string", optional=False),
155
+ _a("params", flatten=True)], read_only=True, category="data"),
156
+
157
+ # --- Order Management ---
158
+ ToolDef("createOrder", "createOrder", "Place an order directly. DESTRUCTIVE -- requires user confirmation.",
159
+ [_a("params", flatten=True)], destructive=True, requires_credentials=True, category="trading"),
160
+ ToolDef("buildOrder", "buildOrder", "Build (sign/preview) an order without submitting.",
161
+ [_a("params", flatten=True)], read_only=True, idempotent=True, requires_credentials=True, category="trading"),
162
+ ToolDef("submitOrder", "submitOrder", "Submit a pre-built order. DESTRUCTIVE -- requires user confirmation.",
163
+ [_a("built", "object", optional=False)], destructive=True, requires_credentials=True, category="trading"),
164
+ ToolDef("cancelOrder", "cancelOrder", "Cancel an open order. DESTRUCTIVE -- requires user confirmation.",
165
+ [_a("orderId", "string", optional=False)], destructive=True, requires_credentials=True, category="trading"),
166
+ ToolDef("fetchOrder", "fetchOrder", "Fetch a single order by ID.",
167
+ [_a("orderId", "string", optional=False)], read_only=True, requires_credentials=True, category="trading"),
168
+ ToolDef("fetchOpenOrders", "fetchOpenOrders", "Fetch open orders, optionally filtered by market.",
169
+ [_a("marketId", "string")], read_only=True, requires_credentials=True, category="trading"),
170
+ ToolDef("fetchClosedOrders", "fetchClosedOrders", "Fetch closed/filled orders.",
171
+ [_a("params", flatten=True)], read_only=True, requires_credentials=True, category="trading"),
172
+ ToolDef("fetchAllOrders", "fetchAllOrders", "Fetch all orders (open + closed).",
173
+ [_a("params", flatten=True)], read_only=True, requires_credentials=True, category="trading"),
174
+ ToolDef("fetchMyTrades", "fetchMyTrades", "Fetch user's filled trades.",
175
+ [_a("params", flatten=True)], read_only=True, requires_credentials=True, category="trading"),
176
+
177
+ # --- Account & Positions ---
178
+ ToolDef("fetchBalance", "fetchBalance", "Get account balance.",
179
+ [_a("address", "string")], read_only=True, requires_credentials=True, category="account"),
180
+ ToolDef("fetchPositions", "fetchPositions", "Get open positions.",
181
+ [_a("address", "string")], read_only=True, requires_credentials=True, category="account"),
182
+
183
+ # --- Router / Cross-Venue ---
184
+ ToolDef("compareMarketPrices", "compareMarketPrices", "Compare live prices across venues side-by-side.",
185
+ [_a("params", flatten=True)], read_only=True, category="router"),
186
+ ToolDef("fetchMarketMatches", "fetchMarketMatches", "Find the same or related market on other venues.",
187
+ [_a("params", flatten=True)], read_only=True, category="router"),
188
+ ToolDef("fetchEventMatches", "fetchEventMatches", "Find matching events across venues.",
189
+ [_a("params", flatten=True)], read_only=True, category="router"),
190
+ ToolDef("fetchRelatedMarkets", "fetchRelatedMarkets", "Fetch markets related to a given market.",
191
+ [_a("params", "object", optional=False)], read_only=True, category="router"),
192
+ ToolDef("fetchMatchedMarkets", "fetchMatchedMarkets", "Fetch all matched market pairs from the catalog.",
193
+ [_a("params", flatten=True)], read_only=True, category="router"),
194
+ ToolDef("fetchMatchedPrices", "fetchMatchedPrices", "Fetch prices for matched market pairs.",
195
+ [_a("params", flatten=True)], read_only=True, category="router"),
196
+ ToolDef("fetchHedges", "fetchHedges", "Find hedging opportunities across venues.",
197
+ [_a("params", "object", optional=False)], read_only=True, category="router"),
198
+ ToolDef("fetchArbitrage", "fetchArbitrage", "Find arbitrage opportunities across venues.",
199
+ [_a("params", flatten=True)], read_only=True, category="router"),
200
+
201
+ # --- Execution & Pricing ---
202
+ ToolDef("getExecutionPrice", "getExecutionPrice",
203
+ "Calculate VWAP execution price from order book.",
204
+ [_a("orderBook", "object", optional=False),
205
+ _a("side", "string", optional=False),
206
+ _a("amount", "number", optional=False)],
207
+ read_only=True, idempotent=True, category="data"),
208
+ ToolDef("getExecutionPriceDetailed", "getExecutionPriceDetailed",
209
+ "Detailed execution price including fill status.",
210
+ [_a("orderBook", "object", optional=False),
211
+ _a("side", "string", optional=False),
212
+ _a("amount", "number", optional=False)],
213
+ read_only=True, idempotent=True, category="data"),
214
+ ]
215
+
216
+ # Index by Python method name
217
+ TOOLS_BY_NAME: dict[str, ToolDef] = {t.method: t for t in TOOLS}
218
+
219
+ # Category lists
220
+ READ_ONLY_TOOLS = [t for t in TOOLS if t.read_only and not t.destructive]
221
+ DESTRUCTIVE_TOOLS = [t for t in TOOLS if t.destructive]
222
+ CREDENTIAL_TOOLS = [t for t in TOOLS if t.requires_credentials]
223
+
224
+
225
+ def get_tool(method: str) -> Optional[ToolDef]:
226
+ """Look up a tool definition by PMXT method name."""
227
+ return TOOLS_BY_NAME.get(method)
228
+
229
+
230
+ def list_tools(category: Optional[str] = None, read_only: Optional[bool] = None) -> list[ToolDef]:
231
+ """List tools, optionally filtered by category or read_only flag."""
232
+ result = TOOLS
233
+ if category:
234
+ result = [t for t in result if t.category == category]
235
+ if read_only is True:
236
+ result = [t for t in result if t.read_only and not t.destructive]
237
+ elif read_only is False:
238
+ result = [t for t in result if not t.read_only or t.destructive]
239
+ return result
240
+
241
+
242
+ def is_destructive(method: str) -> bool:
243
+ """Return True if the method is destructive (requires user confirmation)."""
244
+ tool = get_tool(method)
245
+ return tool is not None and tool.destructive
246
+
247
+
248
+ def requires_credentials(method: str) -> bool:
249
+ """Return True if the method requires exchange credentials."""
250
+ tool = get_tool(method)
251
+ return tool is not None and tool.requires_credentials