worm-sdk 0.9.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.
- worm_sdk/__init__.py +62 -0
- worm_sdk/_version.py +1 -0
- worm_sdk/api/__init__.py +23 -0
- worm_sdk/api/account.py +76 -0
- worm_sdk/api/api_keys.py +29 -0
- worm_sdk/api/events.py +69 -0
- worm_sdk/api/margin.py +388 -0
- worm_sdk/api/markets.py +215 -0
- worm_sdk/api/orders.py +185 -0
- worm_sdk/api/redeems.py +102 -0
- worm_sdk/api/search.py +93 -0
- worm_sdk/api/sports.py +24 -0
- worm_sdk/api/trades.py +44 -0
- worm_sdk/auth/__init__.py +4 -0
- worm_sdk/auth/bootstrap.py +80 -0
- worm_sdk/auth/signer.py +103 -0
- worm_sdk/client.py +116 -0
- worm_sdk/constants.py +64 -0
- worm_sdk/exceptions.py +53 -0
- worm_sdk/http.py +236 -0
- worm_sdk/py.typed +0 -0
- worm_sdk/types/__init__.py +149 -0
- worm_sdk/types/account.py +77 -0
- worm_sdk/types/auth.py +34 -0
- worm_sdk/types/common.py +43 -0
- worm_sdk/types/enums.py +165 -0
- worm_sdk/types/event.py +29 -0
- worm_sdk/types/margin.py +117 -0
- worm_sdk/types/market.py +219 -0
- worm_sdk/types/order.py +60 -0
- worm_sdk/types/redeem.py +30 -0
- worm_sdk/types/search.py +30 -0
- worm_sdk/types/sports.py +24 -0
- worm_sdk/types/trade.py +23 -0
- worm_sdk/wire.py +31 -0
- worm_sdk-0.9.0.dist-info/METADATA +218 -0
- worm_sdk-0.9.0.dist-info/RECORD +39 -0
- worm_sdk-0.9.0.dist-info/WHEEL +4 -0
- worm_sdk-0.9.0.dist-info/licenses/LICENSE +21 -0
worm_sdk/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Worm SDK — Python client for the Worm prediction markets platform."""
|
|
2
|
+
|
|
3
|
+
from worm_sdk._version import __version__
|
|
4
|
+
from worm_sdk.auth.signer import SignerProtocol
|
|
5
|
+
from worm_sdk.client import WormClient
|
|
6
|
+
from worm_sdk.exceptions import (
|
|
7
|
+
AuthenticationRequired,
|
|
8
|
+
SigningError,
|
|
9
|
+
WormAPIError,
|
|
10
|
+
WormSDKError,
|
|
11
|
+
)
|
|
12
|
+
from worm_sdk.types.auth import ApiKeyCredential
|
|
13
|
+
from worm_sdk.types.common import CursorPage, ResponseContext
|
|
14
|
+
from worm_sdk.types.enums import (
|
|
15
|
+
CandleInterval,
|
|
16
|
+
MarginActivityType,
|
|
17
|
+
MarginListSort,
|
|
18
|
+
MarginPolymarketMarketState,
|
|
19
|
+
MarginPositionRequestType,
|
|
20
|
+
MarginSettlementState,
|
|
21
|
+
MarketConfigKind,
|
|
22
|
+
MarketSortOption,
|
|
23
|
+
MarketStateFilter,
|
|
24
|
+
OrderSide,
|
|
25
|
+
OrderStatus,
|
|
26
|
+
OrderType,
|
|
27
|
+
PositionRequestState,
|
|
28
|
+
RedeemState,
|
|
29
|
+
SearchResultType,
|
|
30
|
+
SearchSortOption,
|
|
31
|
+
SearchStateFilter,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"__version__",
|
|
36
|
+
"WormClient",
|
|
37
|
+
"SignerProtocol",
|
|
38
|
+
"ApiKeyCredential",
|
|
39
|
+
"CursorPage",
|
|
40
|
+
"ResponseContext",
|
|
41
|
+
"WormSDKError",
|
|
42
|
+
"WormAPIError",
|
|
43
|
+
"AuthenticationRequired",
|
|
44
|
+
"SigningError",
|
|
45
|
+
"MarginPositionRequestType",
|
|
46
|
+
"MarginListSort",
|
|
47
|
+
"MarginActivityType",
|
|
48
|
+
"MarginSettlementState",
|
|
49
|
+
"MarginPolymarketMarketState",
|
|
50
|
+
"MarketConfigKind",
|
|
51
|
+
"MarketSortOption",
|
|
52
|
+
"MarketStateFilter",
|
|
53
|
+
"CandleInterval",
|
|
54
|
+
"OrderSide",
|
|
55
|
+
"OrderStatus",
|
|
56
|
+
"OrderType",
|
|
57
|
+
"PositionRequestState",
|
|
58
|
+
"RedeemState",
|
|
59
|
+
"SearchResultType",
|
|
60
|
+
"SearchSortOption",
|
|
61
|
+
"SearchStateFilter",
|
|
62
|
+
]
|
worm_sdk/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.0"
|
worm_sdk/api/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from worm_sdk.api.account import AccountAPI
|
|
2
|
+
from worm_sdk.api.api_keys import ApiKeysAPI
|
|
3
|
+
from worm_sdk.api.events import EventsAPI
|
|
4
|
+
from worm_sdk.api.margin import MarginAPI
|
|
5
|
+
from worm_sdk.api.markets import MarketsAPI
|
|
6
|
+
from worm_sdk.api.orders import OrdersAPI
|
|
7
|
+
from worm_sdk.api.redeems import RedeemsAPI
|
|
8
|
+
from worm_sdk.api.search import SearchAPI
|
|
9
|
+
from worm_sdk.api.sports import SportsAPI
|
|
10
|
+
from worm_sdk.api.trades import TradesAPI
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AccountAPI",
|
|
14
|
+
"ApiKeysAPI",
|
|
15
|
+
"EventsAPI",
|
|
16
|
+
"MarginAPI",
|
|
17
|
+
"MarketsAPI",
|
|
18
|
+
"OrdersAPI",
|
|
19
|
+
"RedeemsAPI",
|
|
20
|
+
"SearchAPI",
|
|
21
|
+
"SportsAPI",
|
|
22
|
+
"TradesAPI",
|
|
23
|
+
]
|
worm_sdk/api/account.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Account API — authenticated endpoints for user account data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from worm_sdk import constants
|
|
8
|
+
from worm_sdk.http import SyncHTTPClient
|
|
9
|
+
from worm_sdk.types.account import AccountPortfolioItem, AccountPnlBreakdown, AccountProfile
|
|
10
|
+
from worm_sdk.types.common import CursorPage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccountAPI:
|
|
14
|
+
"""User account endpoints (authenticated)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, http: SyncHTTPClient) -> None:
|
|
17
|
+
self._http = http
|
|
18
|
+
|
|
19
|
+
def get_summary(self) -> AccountProfile:
|
|
20
|
+
"""Return account profile fields for the authenticated user.
|
|
21
|
+
|
|
22
|
+
Includes ``username``, ``twitter_username``, and ``joined_at``.
|
|
23
|
+
"""
|
|
24
|
+
data, _ = self._http.get(endpoint=constants.ACCOUNT_SUMMARY, authenticated=True)
|
|
25
|
+
return AccountProfile.model_validate(obj=data)
|
|
26
|
+
|
|
27
|
+
def get_assets(
|
|
28
|
+
self,
|
|
29
|
+
market_condition_id: str | None = None,
|
|
30
|
+
limit: int = 50,
|
|
31
|
+
cursor: str | None = None,
|
|
32
|
+
) -> CursorPage[AccountPortfolioItem]:
|
|
33
|
+
"""List spot market outcome share balances for the authenticated user.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
market_condition_id: When set, return only share rows for this market.
|
|
37
|
+
limit: Maximum number of share rows to return.
|
|
38
|
+
cursor: Pagination cursor from a previous response.
|
|
39
|
+
"""
|
|
40
|
+
params: dict[str, Any] = {"limit": limit}
|
|
41
|
+
if market_condition_id is not None:
|
|
42
|
+
params["market_condition_id"] = market_condition_id
|
|
43
|
+
if cursor is not None:
|
|
44
|
+
params["cursor"] = cursor
|
|
45
|
+
|
|
46
|
+
data, meta = self._http.get(endpoint=constants.ACCOUNT_ASSETS, params=params, authenticated=True)
|
|
47
|
+
items = [AccountPortfolioItem.model_validate(obj=row) for row in (data or [])]
|
|
48
|
+
return CursorPage[AccountPortfolioItem](
|
|
49
|
+
items=items,
|
|
50
|
+
next_cursor=meta.get("next_cursor"),
|
|
51
|
+
limit=meta.get("limit", limit),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def get_pnl(
|
|
55
|
+
self,
|
|
56
|
+
market_condition_id: str | None = None,
|
|
57
|
+
event_condition_id: str | None = None,
|
|
58
|
+
) -> AccountPnlBreakdown:
|
|
59
|
+
"""Return PnL breakdown for the authenticated user.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
market_condition_id: Restrict PnL to a single market.
|
|
63
|
+
event_condition_id: Restrict PnL to a single event.
|
|
64
|
+
"""
|
|
65
|
+
params: dict[str, Any] = {}
|
|
66
|
+
if market_condition_id is not None:
|
|
67
|
+
params["market_condition_id"] = market_condition_id
|
|
68
|
+
if event_condition_id is not None:
|
|
69
|
+
params["event_condition_id"] = event_condition_id
|
|
70
|
+
|
|
71
|
+
data, _ = self._http.get(
|
|
72
|
+
endpoint=constants.ACCOUNT_PNL,
|
|
73
|
+
params=params or None,
|
|
74
|
+
authenticated=True,
|
|
75
|
+
)
|
|
76
|
+
return AccountPnlBreakdown.model_validate(obj=data)
|
worm_sdk/api/api_keys.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""API Keys API — authenticated endpoints for managing API keys."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from worm_sdk import constants
|
|
6
|
+
from worm_sdk.http import SyncHTTPClient
|
|
7
|
+
from worm_sdk.types.auth import ApiKeyListItem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiKeysAPI:
|
|
11
|
+
"""API key management endpoints (authenticated)."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, http: SyncHTTPClient) -> None:
|
|
14
|
+
self._http = http
|
|
15
|
+
|
|
16
|
+
def list(self) -> list[ApiKeyListItem]:
|
|
17
|
+
"""List API keys owned by the authenticated account."""
|
|
18
|
+
data, _ = self._http.get(endpoint=constants.AUTH_KEYS_LIST, authenticated=True)
|
|
19
|
+
rows = data or []
|
|
20
|
+
return [ApiKeyListItem.model_validate(obj=row) for row in rows]
|
|
21
|
+
|
|
22
|
+
def revoke(self, key_id: str) -> ApiKeyListItem:
|
|
23
|
+
"""Revoke an API key by ``key_id``.
|
|
24
|
+
|
|
25
|
+
Revoked keys can no longer authenticate requests.
|
|
26
|
+
"""
|
|
27
|
+
url = constants.AUTH_KEYS_REVOKE.format(key_id=key_id)
|
|
28
|
+
data, _ = self._http.delete(endpoint=url, authenticated=True)
|
|
29
|
+
return ApiKeyListItem.model_validate(obj=data)
|
worm_sdk/api/events.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Events API — public endpoints for event data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from worm_sdk import constants
|
|
8
|
+
from worm_sdk.http import SyncHTTPClient
|
|
9
|
+
from worm_sdk.types.common import CursorPage
|
|
10
|
+
from worm_sdk.types.enums import MarketSortOption, MarketStateFilter
|
|
11
|
+
from worm_sdk.types.event import Event, EventDetail
|
|
12
|
+
from worm_sdk.wire import enum_wire_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventsAPI:
|
|
16
|
+
"""Public event data endpoints."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, http: SyncHTTPClient) -> None:
|
|
19
|
+
self._http = http
|
|
20
|
+
|
|
21
|
+
def list(
|
|
22
|
+
self,
|
|
23
|
+
state: MarketStateFilter | None = None,
|
|
24
|
+
category: str | None = None,
|
|
25
|
+
sort: MarketSortOption | str | None = None,
|
|
26
|
+
sport: str | None = None,
|
|
27
|
+
league: str | None = None,
|
|
28
|
+
limit: int = 20,
|
|
29
|
+
cursor: str | None = None,
|
|
30
|
+
) -> CursorPage[Event]:
|
|
31
|
+
"""List public events with filtering and cursor pagination.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
state: Filter by market lifecycle state for events.
|
|
35
|
+
category: Filter by event category slug.
|
|
36
|
+
sort: Sort key: ``market_cap``, ``last_trade``, ``total_volume``, ``live_stream``,
|
|
37
|
+
``trending``, ``leverage``, or ``ending_soon`` (see ``MarketSortOption``).
|
|
38
|
+
sport: Comma-separated sport slugs from ``client.sports.list()``.
|
|
39
|
+
league: Comma-separated league slugs from the sports catalog; requires ``sport``.
|
|
40
|
+
limit: Maximum number of events to return.
|
|
41
|
+
cursor: Pagination cursor from a previous response.
|
|
42
|
+
"""
|
|
43
|
+
params: dict[str, Any] = {"limit": limit}
|
|
44
|
+
if cursor is not None:
|
|
45
|
+
params["cursor"] = cursor
|
|
46
|
+
if state is not None:
|
|
47
|
+
params["state"] = state.value
|
|
48
|
+
if category is not None:
|
|
49
|
+
params["category"] = category
|
|
50
|
+
if sort is not None:
|
|
51
|
+
params["sort"] = enum_wire_value(value=sort, enum_cls=MarketSortOption)
|
|
52
|
+
if sport is not None:
|
|
53
|
+
params["sport"] = sport
|
|
54
|
+
if league is not None:
|
|
55
|
+
params["league"] = league
|
|
56
|
+
|
|
57
|
+
data, meta = self._http.get(endpoint=constants.EVENTS_LIST, params=params)
|
|
58
|
+
items = [Event.model_validate(obj=e) for e in (data or [])]
|
|
59
|
+
return CursorPage[Event](
|
|
60
|
+
items=items,
|
|
61
|
+
next_cursor=meta.get("next_cursor"),
|
|
62
|
+
limit=meta.get("limit", limit),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def get(self, condition_id: str) -> EventDetail:
|
|
66
|
+
"""Fetch full event details by event ``condition_id``."""
|
|
67
|
+
url = constants.EVENT_DETAIL.format(condition_id=condition_id)
|
|
68
|
+
data, _ = self._http.get(endpoint=url)
|
|
69
|
+
return EventDetail.model_validate(obj=data)
|
worm_sdk/api/margin.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Margin API — authenticated endpoints for margin position management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from worm_sdk import constants
|
|
10
|
+
from worm_sdk.auth.signer import SignerProtocol
|
|
11
|
+
from worm_sdk.http import SyncHTTPClient
|
|
12
|
+
from worm_sdk.types.common import CursorPage
|
|
13
|
+
from worm_sdk.types.enums import (
|
|
14
|
+
MarginListSort,
|
|
15
|
+
MarginPolymarketMarketState,
|
|
16
|
+
MarginPositionRequestType,
|
|
17
|
+
MarginSettlementState,
|
|
18
|
+
PositionRequestState,
|
|
19
|
+
)
|
|
20
|
+
from worm_sdk.types.margin import (
|
|
21
|
+
MarginCloseResponse,
|
|
22
|
+
MarginEstimate,
|
|
23
|
+
MarginSettlementClaimResponse,
|
|
24
|
+
Position,
|
|
25
|
+
PositionRequest,
|
|
26
|
+
PositionTpSl,
|
|
27
|
+
Settlement,
|
|
28
|
+
)
|
|
29
|
+
from worm_sdk.wire import comma_join_enum_wire, enum_wire_value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MarginAPI:
|
|
33
|
+
"""Margin trading endpoints."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, http: SyncHTTPClient, signer: SignerProtocol | None = None) -> None:
|
|
36
|
+
self._http = http
|
|
37
|
+
self._signer = signer
|
|
38
|
+
|
|
39
|
+
# --- Public ---
|
|
40
|
+
|
|
41
|
+
def estimate(
|
|
42
|
+
self,
|
|
43
|
+
market_condition_id: str,
|
|
44
|
+
funds: Decimal | str | int,
|
|
45
|
+
leverage: Decimal | str | int | None = None,
|
|
46
|
+
is_yes: bool = True,
|
|
47
|
+
) -> MarginEstimate:
|
|
48
|
+
"""Estimate margin position metrics before opening a position.
|
|
49
|
+
|
|
50
|
+
Public endpoint; authentication is not required.
|
|
51
|
+
"""
|
|
52
|
+
params: dict[str, Any] = {
|
|
53
|
+
"market_condition_id": market_condition_id,
|
|
54
|
+
"funds": str(funds),
|
|
55
|
+
"is_yes": is_yes,
|
|
56
|
+
}
|
|
57
|
+
if leverage is not None:
|
|
58
|
+
params["leverage"] = str(leverage)
|
|
59
|
+
|
|
60
|
+
data, _ = self._http.get(endpoint=constants.MARGIN_ESTIMATE, params=params)
|
|
61
|
+
return MarginEstimate.model_validate(obj=data)
|
|
62
|
+
|
|
63
|
+
# --- Positions ---
|
|
64
|
+
|
|
65
|
+
def list_positions(
|
|
66
|
+
self,
|
|
67
|
+
market_condition_id: str | None = None,
|
|
68
|
+
is_closed: bool | None = None,
|
|
69
|
+
sort: MarginListSort | str = MarginListSort.CREATED_DESC,
|
|
70
|
+
limit: int = 50,
|
|
71
|
+
cursor: str | None = None,
|
|
72
|
+
) -> CursorPage[Position]:
|
|
73
|
+
"""List margin positions for the authenticated user.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
market_condition_id: Filter to a specific market.
|
|
77
|
+
is_closed: ``True`` for closed only, ``False`` for open only, ``None`` for both.
|
|
78
|
+
sort: ``created`` or ``-created`` (default ``-created``); see ``MarginListSort``.
|
|
79
|
+
limit: Maximum number of positions to return.
|
|
80
|
+
cursor: Pagination cursor from a previous response.
|
|
81
|
+
"""
|
|
82
|
+
params: dict[str, Any] = {
|
|
83
|
+
"limit": limit,
|
|
84
|
+
"sort": enum_wire_value(value=sort, enum_cls=MarginListSort),
|
|
85
|
+
}
|
|
86
|
+
if cursor is not None:
|
|
87
|
+
params["cursor"] = cursor
|
|
88
|
+
if market_condition_id is not None:
|
|
89
|
+
params["market_condition_id"] = market_condition_id
|
|
90
|
+
if is_closed is not None:
|
|
91
|
+
params["is_closed"] = is_closed
|
|
92
|
+
|
|
93
|
+
data, meta = self._http.get(endpoint=constants.MARGIN_POSITIONS, params=params, authenticated=True)
|
|
94
|
+
items = [Position.model_validate(obj=row) for row in (data or [])]
|
|
95
|
+
return CursorPage[Position](
|
|
96
|
+
items=items,
|
|
97
|
+
next_cursor=meta.get("next_cursor"),
|
|
98
|
+
limit=meta.get("limit", limit),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def get_position(self, pubkey: str) -> Position:
|
|
102
|
+
"""Fetch a single margin position by ``pubkey``."""
|
|
103
|
+
url = constants.MARGIN_POSITION_DETAIL.format(pubkey=pubkey)
|
|
104
|
+
data, _ = self._http.get(endpoint=url, authenticated=True)
|
|
105
|
+
return Position.model_validate(obj=data)
|
|
106
|
+
|
|
107
|
+
def close_position(
|
|
108
|
+
self,
|
|
109
|
+
pubkey: str,
|
|
110
|
+
price: Decimal | str | int | None = None,
|
|
111
|
+
) -> MarginCloseResponse:
|
|
112
|
+
"""Close a margin position.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
pubkey: Position identifier.
|
|
116
|
+
price: Optional limit/target close price.
|
|
117
|
+
"""
|
|
118
|
+
url = constants.MARGIN_POSITION_CLOSE.format(pubkey=pubkey)
|
|
119
|
+
params: dict[str, Any] = {}
|
|
120
|
+
if price is not None:
|
|
121
|
+
params["price"] = str(price)
|
|
122
|
+
data, _ = self._http.delete(endpoint=url, params=params or None, authenticated=True)
|
|
123
|
+
return MarginCloseResponse.model_validate(obj=data)
|
|
124
|
+
|
|
125
|
+
# --- Position Requests ---
|
|
126
|
+
|
|
127
|
+
def list_requests(
|
|
128
|
+
self,
|
|
129
|
+
market_condition_id: str | None = None,
|
|
130
|
+
states: PositionRequestState | str | Iterable[PositionRequestState] | None = None,
|
|
131
|
+
is_yes: bool | None = None,
|
|
132
|
+
leverage: Decimal | str | int | None = None,
|
|
133
|
+
sort: MarginListSort | str = MarginListSort.CREATED_DESC,
|
|
134
|
+
limit: int = 50,
|
|
135
|
+
cursor: str | None = None,
|
|
136
|
+
) -> CursorPage[PositionRequest]:
|
|
137
|
+
"""List margin position requests for the authenticated user.
|
|
138
|
+
|
|
139
|
+
Use this to inspect pending, submitted, filled, or canceled requests.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
market_condition_id: Filter to a specific market.
|
|
143
|
+
states: Comma-separated request state filters (see ``PositionRequestState`` wire values)
|
|
144
|
+
or an iterable of enum members.
|
|
145
|
+
is_yes: Filter by outcome side.
|
|
146
|
+
leverage: Filter by leverage value.
|
|
147
|
+
sort: ``created`` or ``-created`` (default ``-created``); see ``MarginListSort``.
|
|
148
|
+
limit: Maximum number of requests to return.
|
|
149
|
+
cursor: Pagination cursor from a previous response.
|
|
150
|
+
"""
|
|
151
|
+
params: dict[str, Any] = {
|
|
152
|
+
"limit": limit,
|
|
153
|
+
"sort": enum_wire_value(value=sort, enum_cls=MarginListSort),
|
|
154
|
+
}
|
|
155
|
+
if cursor is not None:
|
|
156
|
+
params["cursor"] = cursor
|
|
157
|
+
if market_condition_id is not None:
|
|
158
|
+
params["market_condition_id"] = market_condition_id
|
|
159
|
+
if states is not None:
|
|
160
|
+
params["states"] = comma_join_enum_wire(value=states, enum_cls=PositionRequestState)
|
|
161
|
+
if is_yes is not None:
|
|
162
|
+
params["is_yes"] = is_yes
|
|
163
|
+
if leverage is not None:
|
|
164
|
+
params["leverage"] = str(leverage)
|
|
165
|
+
|
|
166
|
+
data, meta = self._http.get(endpoint=constants.MARGIN_REQUESTS, params=params, authenticated=True)
|
|
167
|
+
items = [PositionRequest.model_validate(obj=row) for row in (data or [])]
|
|
168
|
+
return CursorPage[PositionRequest](
|
|
169
|
+
items=items,
|
|
170
|
+
next_cursor=meta.get("next_cursor"),
|
|
171
|
+
limit=meta.get("limit", limit),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def get_request(self, pubkey: str) -> PositionRequest:
|
|
175
|
+
"""Fetch a single margin request by ``pubkey``."""
|
|
176
|
+
url = constants.MARGIN_REQUEST_DETAIL.format(pubkey=pubkey)
|
|
177
|
+
data, _ = self._http.get(endpoint=url, authenticated=True)
|
|
178
|
+
return PositionRequest.model_validate(obj=data)
|
|
179
|
+
|
|
180
|
+
def create_request(
|
|
181
|
+
self,
|
|
182
|
+
market_condition_id: str,
|
|
183
|
+
position_request_type: MarginPositionRequestType | str,
|
|
184
|
+
funds: Decimal | str | int | None = None,
|
|
185
|
+
is_yes: bool = True,
|
|
186
|
+
leverage: Decimal | str | int | None = None,
|
|
187
|
+
price: Decimal | str | int | None = None,
|
|
188
|
+
shares: Decimal | str | int | None = None,
|
|
189
|
+
take_profit_price: Decimal | str | int | None = None,
|
|
190
|
+
stop_loss_price: Decimal | str | int | None = None,
|
|
191
|
+
) -> PositionRequest:
|
|
192
|
+
"""Create an unsigned margin position request draft.
|
|
193
|
+
|
|
194
|
+
``position_request_type`` must be ``market`` (pass ``funds``) or ``limit``
|
|
195
|
+
(pass ``price`` and ``shares``). The API rejects mixed combinations.
|
|
196
|
+
|
|
197
|
+
The response includes ``pubkey`` and signing ``message``. Sign and submit
|
|
198
|
+
with ``submit_request()``.
|
|
199
|
+
"""
|
|
200
|
+
ptype = self._margin_request_type_json(value=position_request_type)
|
|
201
|
+
payload: dict[str, Any] = {
|
|
202
|
+
"type": ptype,
|
|
203
|
+
"market_condition_id": market_condition_id,
|
|
204
|
+
"is_yes": is_yes,
|
|
205
|
+
}
|
|
206
|
+
if leverage is not None:
|
|
207
|
+
payload["leverage"] = str(leverage)
|
|
208
|
+
if ptype == MarginPositionRequestType.MARKET.value:
|
|
209
|
+
if funds is None:
|
|
210
|
+
raise ValueError("funds is required when position_request_type is market")
|
|
211
|
+
payload["funds"] = str(funds)
|
|
212
|
+
else:
|
|
213
|
+
if price is None or shares is None:
|
|
214
|
+
raise ValueError("price and shares are required when position_request_type is limit")
|
|
215
|
+
payload["price"] = str(price)
|
|
216
|
+
payload["shares"] = str(shares)
|
|
217
|
+
if take_profit_price is not None:
|
|
218
|
+
payload["take_profit_price"] = str(take_profit_price)
|
|
219
|
+
if stop_loss_price is not None:
|
|
220
|
+
payload["stop_loss_price"] = str(stop_loss_price)
|
|
221
|
+
|
|
222
|
+
data, _ = self._http.post(endpoint=constants.MARGIN_REQUESTS, json=payload, authenticated=True)
|
|
223
|
+
return PositionRequest.model_validate(obj=data)
|
|
224
|
+
|
|
225
|
+
def cancel_request(self, pubkey: str) -> PositionRequest:
|
|
226
|
+
"""Cancel a pending margin request by ``pubkey``."""
|
|
227
|
+
url = constants.MARGIN_REQUEST_DETAIL.format(pubkey=pubkey)
|
|
228
|
+
data, _ = self._http.delete(endpoint=url, authenticated=True)
|
|
229
|
+
return PositionRequest.model_validate(obj=data)
|
|
230
|
+
|
|
231
|
+
def submit_request(
|
|
232
|
+
self,
|
|
233
|
+
pubkey: str,
|
|
234
|
+
signature: str,
|
|
235
|
+
) -> PositionRequest:
|
|
236
|
+
"""Submit a hex signature over the position-request draft ``message``."""
|
|
237
|
+
url = constants.MARGIN_REQUEST_SUBMIT.format(pubkey=pubkey)
|
|
238
|
+
data, _ = self._http.post(
|
|
239
|
+
endpoint=url,
|
|
240
|
+
json={"signature": signature},
|
|
241
|
+
authenticated=True,
|
|
242
|
+
)
|
|
243
|
+
return PositionRequest.model_validate(obj=data)
|
|
244
|
+
|
|
245
|
+
def open_position(
|
|
246
|
+
self,
|
|
247
|
+
market_condition_id: str,
|
|
248
|
+
position_request_type: MarginPositionRequestType | str,
|
|
249
|
+
funds: Decimal | str | int | None = None,
|
|
250
|
+
is_yes: bool = True,
|
|
251
|
+
leverage: Decimal | str | int | None = None,
|
|
252
|
+
price: Decimal | str | int | None = None,
|
|
253
|
+
shares: Decimal | str | int | None = None,
|
|
254
|
+
take_profit_price: Decimal | str | int | None = None,
|
|
255
|
+
stop_loss_price: Decimal | str | int | None = None,
|
|
256
|
+
) -> PositionRequest:
|
|
257
|
+
"""Convenience: create request + auto-sign + submit in one call.
|
|
258
|
+
|
|
259
|
+
Requires a ``signer`` configured on ``WormClient``.
|
|
260
|
+
"""
|
|
261
|
+
from worm_sdk.exceptions import SigningError
|
|
262
|
+
|
|
263
|
+
if self._signer is None:
|
|
264
|
+
raise SigningError("A signer is required for open_position(). Pass signer= to WormClient.")
|
|
265
|
+
|
|
266
|
+
request_data = self.create_request(
|
|
267
|
+
market_condition_id=market_condition_id,
|
|
268
|
+
position_request_type=position_request_type,
|
|
269
|
+
funds=funds,
|
|
270
|
+
is_yes=is_yes,
|
|
271
|
+
leverage=leverage,
|
|
272
|
+
price=price,
|
|
273
|
+
shares=shares,
|
|
274
|
+
take_profit_price=take_profit_price,
|
|
275
|
+
stop_loss_price=stop_loss_price,
|
|
276
|
+
)
|
|
277
|
+
message = request_data.message
|
|
278
|
+
if not message:
|
|
279
|
+
raise SigningError("Margin request draft did not include a signable message.")
|
|
280
|
+
if not request_data.pubkey:
|
|
281
|
+
raise SigningError("Margin request draft did not include a pubkey.")
|
|
282
|
+
signature = self._signer.sign_transaction(transaction_hex=message)
|
|
283
|
+
return self.submit_request(pubkey=request_data.pubkey, signature=signature)
|
|
284
|
+
|
|
285
|
+
# --- TP/SL ---
|
|
286
|
+
|
|
287
|
+
def set_tp_sl(
|
|
288
|
+
self,
|
|
289
|
+
pubkey: str,
|
|
290
|
+
take_profit_price: Decimal | str | int | None = None,
|
|
291
|
+
stop_loss_price: Decimal | str | int | None = None,
|
|
292
|
+
) -> PositionTpSl:
|
|
293
|
+
"""Set or update take-profit / stop-loss values on a position."""
|
|
294
|
+
payload: dict[str, Any] = {}
|
|
295
|
+
if take_profit_price is not None:
|
|
296
|
+
payload["take_profit_price"] = str(take_profit_price)
|
|
297
|
+
if stop_loss_price is not None:
|
|
298
|
+
payload["stop_loss_price"] = str(stop_loss_price)
|
|
299
|
+
if not any(k in payload for k in ("take_profit_price", "stop_loss_price")):
|
|
300
|
+
raise ValueError("Provide at least one of take_profit_price= or stop_loss_price=.")
|
|
301
|
+
|
|
302
|
+
url = constants.MARGIN_POSITION_TP_SL.format(pubkey=pubkey)
|
|
303
|
+
data, _ = self._http.post(endpoint=url, json=payload, authenticated=True)
|
|
304
|
+
return PositionTpSl.model_validate(obj=data)
|
|
305
|
+
|
|
306
|
+
def remove_tp_sl(self, pubkey: str) -> PositionTpSl:
|
|
307
|
+
"""Remove both take-profit and stop-loss settings from a position."""
|
|
308
|
+
url = constants.MARGIN_POSITION_TP_SL.format(pubkey=pubkey)
|
|
309
|
+
data, _ = self._http.delete(endpoint=url, authenticated=True)
|
|
310
|
+
return PositionTpSl.model_validate(obj=data)
|
|
311
|
+
|
|
312
|
+
# --- Settlements ---
|
|
313
|
+
|
|
314
|
+
def claim_settlement(self, pubkey: str) -> MarginSettlementClaimResponse:
|
|
315
|
+
"""Claim settlement funds for an eligible position."""
|
|
316
|
+
url = constants.MARGIN_POSITION_SETTLEMENTS_ACTION.format(pubkey=pubkey)
|
|
317
|
+
data, _ = self._http.post(endpoint=url, authenticated=True)
|
|
318
|
+
return MarginSettlementClaimResponse.model_validate(obj=data)
|
|
319
|
+
|
|
320
|
+
def list_settlements(
|
|
321
|
+
self,
|
|
322
|
+
position_pubkey: str | None = None,
|
|
323
|
+
market_condition_id: str | None = None,
|
|
324
|
+
states: MarginSettlementState | str | Iterable[MarginSettlementState] | None = None,
|
|
325
|
+
market_state: MarginPolymarketMarketState | str | None = None,
|
|
326
|
+
position_leverage: Decimal | str | int | None = None,
|
|
327
|
+
sort: MarginListSort | str = MarginListSort.CREATED_DESC,
|
|
328
|
+
limit: int = 50,
|
|
329
|
+
cursor: str | None = None,
|
|
330
|
+
) -> CursorPage[Settlement]:
|
|
331
|
+
"""List settlement records across the user's margin positions.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
position_pubkey: Filter by margin position pubkey.
|
|
335
|
+
market_condition_id: Filter by market.
|
|
336
|
+
states: Comma-separated settlement state filters (see ``MarginSettlementState``) or
|
|
337
|
+
an iterable of enum members.
|
|
338
|
+
market_state: Market lifecycle filter for this list (``open`` / ``resolved``).
|
|
339
|
+
position_leverage: Filter by position leverage.
|
|
340
|
+
sort: ``created`` or ``-created`` (default ``-created``).
|
|
341
|
+
limit: Page size.
|
|
342
|
+
cursor: Pagination cursor.
|
|
343
|
+
"""
|
|
344
|
+
params: dict[str, Any] = {
|
|
345
|
+
"limit": limit,
|
|
346
|
+
"sort": enum_wire_value(value=sort, enum_cls=MarginListSort),
|
|
347
|
+
}
|
|
348
|
+
if cursor is not None:
|
|
349
|
+
params["cursor"] = cursor
|
|
350
|
+
if position_pubkey is not None:
|
|
351
|
+
params["position_pubkey"] = position_pubkey
|
|
352
|
+
if market_condition_id is not None:
|
|
353
|
+
params["market_condition_id"] = market_condition_id
|
|
354
|
+
if states is not None:
|
|
355
|
+
params["states"] = comma_join_enum_wire(value=states, enum_cls=MarginSettlementState)
|
|
356
|
+
if market_state is not None:
|
|
357
|
+
params["market_state"] = self._normalize_market_state_wire(value=market_state)
|
|
358
|
+
if position_leverage is not None:
|
|
359
|
+
params["position_leverage"] = str(position_leverage)
|
|
360
|
+
|
|
361
|
+
data, meta = self._http.get(endpoint=constants.MARGIN_SETTLEMENTS, params=params, authenticated=True)
|
|
362
|
+
items = [Settlement.model_validate(obj=row) for row in (data or [])]
|
|
363
|
+
return CursorPage[Settlement](
|
|
364
|
+
items=items,
|
|
365
|
+
next_cursor=meta.get("next_cursor"),
|
|
366
|
+
limit=meta.get("limit", limit),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _margin_request_type_json(value: MarginPositionRequestType | str) -> str:
|
|
371
|
+
normalized = enum_wire_value(value=value, enum_cls=MarginPositionRequestType)
|
|
372
|
+
if normalized not in (
|
|
373
|
+
MarginPositionRequestType.MARKET.value,
|
|
374
|
+
MarginPositionRequestType.LIMIT.value,
|
|
375
|
+
):
|
|
376
|
+
raise ValueError(f'position_request_type must be market or limit, got {value!r}')
|
|
377
|
+
return normalized
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _normalize_market_state_wire(value: MarginPolymarketMarketState | str) -> str:
|
|
381
|
+
normalized = enum_wire_value(value=value, enum_cls=MarginPolymarketMarketState)
|
|
382
|
+
allowed = {
|
|
383
|
+
MarginPolymarketMarketState.OPEN.value,
|
|
384
|
+
MarginPolymarketMarketState.RESOLVED.value,
|
|
385
|
+
}
|
|
386
|
+
if normalized not in allowed:
|
|
387
|
+
raise ValueError(f"market_state must be open or resolved, got {value!r}")
|
|
388
|
+
return normalized
|