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.
- hermes_pmxt/__init__.py +101 -0
- hermes_pmxt/config.py +145 -0
- hermes_pmxt/exchanges.py +187 -0
- hermes_pmxt/registry.py +251 -0
- hermes_pmxt/shaper.py +255 -0
- hermes_pmxt/tools.py +1463 -0
- hermes_pmxt-0.3.0.dist-info/METADATA +269 -0
- hermes_pmxt-0.3.0.dist-info/RECORD +10 -0
- hermes_pmxt-0.3.0.dist-info/WHEEL +4 -0
- hermes_pmxt-0.3.0.dist-info/licenses/LICENSE +21 -0
hermes_pmxt/__init__.py
ADDED
|
@@ -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)
|
hermes_pmxt/exchanges.py
ADDED
|
@@ -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 []
|
hermes_pmxt/registry.py
ADDED
|
@@ -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
|