prediction-market-agent-tooling 0.65.5__py3-none-any.whl → 0.69.17.dev1149__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.
- prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
- prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
- prediction_market_agent_tooling/abis/processor.abi.json +16 -0
- prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
- prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
- prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
- prediction_market_agent_tooling/benchmark/utils.py +13 -0
- prediction_market_agent_tooling/chains.py +1 -0
- prediction_market_agent_tooling/config.py +61 -2
- prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
- prediction_market_agent_tooling/deploy/agent.py +199 -67
- prediction_market_agent_tooling/deploy/agent_example.py +1 -1
- prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
- prediction_market_agent_tooling/deploy/constants.py +6 -0
- prediction_market_agent_tooling/gtypes.py +11 -1
- prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
- prediction_market_agent_tooling/loggers.py +9 -1
- prediction_market_agent_tooling/logprobs_parser.py +2 -1
- prediction_market_agent_tooling/markets/agent_market.py +106 -18
- prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
- prediction_market_agent_tooling/markets/data_models.py +120 -7
- prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
- prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
- prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
- prediction_market_agent_tooling/markets/market_type.py +74 -0
- prediction_market_agent_tooling/markets/markets.py +7 -99
- prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
- prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
- prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
- prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
- prediction_market_agent_tooling/markets/omen/omen.py +112 -23
- prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
- prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
- prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
- prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
- prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
- prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
- prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
- prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
- prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
- prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
- prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
- prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
- prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
- prediction_market_agent_tooling/markets/seer/seer.py +393 -106
- prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
- prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
- prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
- prediction_market_agent_tooling/tools/_generic_value.py +8 -2
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
- prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
- prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
- prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
- prediction_market_agent_tooling/tools/contract.py +480 -38
- prediction_market_agent_tooling/tools/contract_utils.py +61 -0
- prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
- prediction_market_agent_tooling/tools/cow/models.py +122 -0
- prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
- prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
- prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
- prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
- prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
- prediction_market_agent_tooling/tools/openai_utils.py +31 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
- prediction_market_agent_tooling/tools/rephrase.py +71 -0
- prediction_market_agent_tooling/tools/singleton.py +11 -6
- prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
- prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
- prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
- prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
- prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
- prediction_market_agent_tooling/tools/utils.py +61 -3
- prediction_market_agent_tooling/tools/web3_utils.py +63 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
- prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,140 +1,163 @@
|
|
|
1
1
|
import typing as t
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from urllib.parse import urljoin
|
|
2
5
|
|
|
3
|
-
import
|
|
6
|
+
import httpx
|
|
4
7
|
import tenacity
|
|
5
8
|
|
|
9
|
+
from prediction_market_agent_tooling.gtypes import ChecksumAddress, HexBytes
|
|
6
10
|
from prediction_market_agent_tooling.loggers import logger
|
|
7
11
|
from prediction_market_agent_tooling.markets.polymarket.data_models import (
|
|
8
12
|
POLYMARKET_FALSE_OUTCOME,
|
|
9
13
|
POLYMARKET_TRUE_OUTCOME,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
PolymarketPriceResponse,
|
|
14
|
-
PolymarketTokenWithPrices,
|
|
15
|
-
Prices,
|
|
14
|
+
PolymarketGammaResponse,
|
|
15
|
+
PolymarketGammaResponseDataItem,
|
|
16
|
+
PolymarketPositionResponse,
|
|
16
17
|
)
|
|
18
|
+
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
|
19
|
+
from prediction_market_agent_tooling.tools.httpx_cached_client import HttpxCachedClient
|
|
17
20
|
from prediction_market_agent_tooling.tools.utils import response_to_model
|
|
18
21
|
|
|
19
|
-
POLYMARKET_API_BASE_URL = "https://clob.polymarket.com/"
|
|
20
22
|
MARKETS_LIMIT = 100 # Polymarket will only return up to 100 markets
|
|
23
|
+
POLYMARKET_GAMMA_API_BASE_URL = "https://gamma-api.polymarket.com/"
|
|
21
24
|
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_polymarkets(
|
|
29
|
-
limit: int,
|
|
30
|
-
with_rewards: bool = False,
|
|
31
|
-
next_cursor: str | None = None,
|
|
32
|
-
) -> MarketsEndpointResponse:
|
|
33
|
-
url = (
|
|
34
|
-
f"{POLYMARKET_API_BASE_URL}/{'sampling-markets' if with_rewards else 'markets'}"
|
|
35
|
-
)
|
|
36
|
-
params: dict[str, str | int | float | None] = {
|
|
37
|
-
"limit": min(limit, MARKETS_LIMIT),
|
|
38
|
-
}
|
|
39
|
-
if next_cursor is not None:
|
|
40
|
-
params["next_cursor"] = next_cursor
|
|
41
|
-
return response_to_model(requests.get(url, params=params), MarketsEndpointResponse)
|
|
26
|
+
class PolymarketOrderByEnum(str, Enum):
|
|
27
|
+
LIQUIDITY = "liquidity"
|
|
28
|
+
START_DATE = "startDate"
|
|
29
|
+
END_DATE = "endDate"
|
|
30
|
+
VOLUME_24HR = "volume24hr"
|
|
42
31
|
|
|
43
32
|
|
|
44
|
-
|
|
33
|
+
@tenacity.retry(
|
|
34
|
+
stop=tenacity.stop_after_attempt(2),
|
|
35
|
+
wait=tenacity.wait_fixed(1),
|
|
36
|
+
after=lambda x: logger.debug(
|
|
37
|
+
f"get_polymarkets_with_pagination failed, {x.attempt_number=}."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
def get_polymarkets_with_pagination(
|
|
45
41
|
limit: int,
|
|
46
|
-
|
|
42
|
+
created_after: t.Optional[DatetimeUTC] = None,
|
|
43
|
+
active: bool | None = None,
|
|
44
|
+
closed: bool | None = None,
|
|
47
45
|
excluded_questions: set[str] | None = None,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
only_binary: bool = True,
|
|
47
|
+
archived: bool = False,
|
|
48
|
+
ascending: bool = False,
|
|
49
|
+
order_by: PolymarketOrderByEnum = PolymarketOrderByEnum.VOLUME_24HR,
|
|
50
|
+
) -> list[PolymarketGammaResponseDataItem]:
|
|
51
51
|
"""
|
|
52
|
-
|
|
52
|
+
Binary markets have len(model.markets) == 1.
|
|
53
|
+
Categorical markets have len(model.markets) > 1
|
|
53
54
|
"""
|
|
54
|
-
|
|
55
|
-
all_markets: list[
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
client: httpx.Client = HttpxCachedClient(ttl=timedelta(seconds=60)).get_client()
|
|
56
|
+
all_markets: list[PolymarketGammaResponseDataItem] = []
|
|
57
|
+
offset = 0
|
|
58
|
+
remaining = limit
|
|
59
|
+
|
|
60
|
+
while remaining > 0:
|
|
61
|
+
# Calculate how many items to request in this batch (up to MARKETS_LIMIT or remaining)
|
|
62
|
+
# By default we fetch many markets because not possible to filter by binary/categorical
|
|
63
|
+
batch_size = MARKETS_LIMIT
|
|
64
|
+
|
|
65
|
+
# Build query parameters, excluding None values
|
|
66
|
+
params = {
|
|
67
|
+
"limit": batch_size,
|
|
68
|
+
"active": str(active).lower() if active is not None else None,
|
|
69
|
+
"archived": str(archived).lower(),
|
|
70
|
+
"closed": str(closed).lower() if closed is not None else None,
|
|
71
|
+
"order": order_by.value,
|
|
72
|
+
"ascending": str(ascending).lower(),
|
|
73
|
+
"offset": offset,
|
|
74
|
+
}
|
|
75
|
+
params_not_none = {k: v for k, v in params.items() if v is not None}
|
|
76
|
+
url = urljoin(
|
|
77
|
+
POLYMARKET_GAMMA_API_BASE_URL,
|
|
78
|
+
f"events/pagination",
|
|
61
79
|
)
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if closed is not None and market.closed != closed:
|
|
66
|
-
continue
|
|
81
|
+
r = client.get(url, params=params_not_none)
|
|
82
|
+
r.raise_for_status()
|
|
67
83
|
|
|
68
|
-
|
|
69
|
-
# Documentation does not provide more details about this, but if API returns them, website gives "Oops...we didn't forecast this".
|
|
70
|
-
if not market.active:
|
|
71
|
-
continue
|
|
84
|
+
market_response = response_to_model(r, PolymarketGammaResponse)
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
markets_to_add = []
|
|
87
|
+
for m in market_response.data:
|
|
88
|
+
# Some Polymarket markets are missing the markets field
|
|
89
|
+
if m.markets is None or m.markets[0].clobTokenIds is None:
|
|
76
90
|
continue
|
|
77
|
-
|
|
78
|
-
if excluded_questions and market.question in excluded_questions:
|
|
91
|
+
if excluded_questions and m.title in excluded_questions:
|
|
79
92
|
continue
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
if
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
sorted_outcome_list = sorted(m.markets[0].outcomes_list)
|
|
95
|
+
if only_binary:
|
|
96
|
+
# We keep markets that are only Yes,No
|
|
97
|
+
if len(m.markets) > 1 or sorted_outcome_list != [
|
|
98
|
+
POLYMARKET_FALSE_OUTCOME,
|
|
99
|
+
POLYMARKET_TRUE_OUTCOME,
|
|
100
|
+
]:
|
|
101
|
+
continue
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
# TODO: Add support for `description` for `AgentMarket` and if it isn't None, use it in addition to the question in all agents. Then this can be removed.
|
|
90
|
-
if main_markets_only and not market.fetch_if_its_a_main_market():
|
|
103
|
+
if not m.startDate or (created_after and created_after > m.startDate):
|
|
91
104
|
continue
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
market_with_prices = PolymarketMarketWithPrices.model_validate(
|
|
95
|
-
{**market.model_dump(), "tokens": tokens_with_price}
|
|
96
|
-
)
|
|
106
|
+
markets_to_add.append(m)
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
if only_binary:
|
|
109
|
+
markets_to_add = [
|
|
110
|
+
market
|
|
111
|
+
for market in markets_to_add
|
|
112
|
+
if market.markets is not None and len(market.markets) == 1
|
|
113
|
+
]
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
# Add the markets from this batch to our results
|
|
116
|
+
all_markets.extend(markets_to_add)
|
|
102
117
|
|
|
103
|
-
|
|
118
|
+
# Update counters
|
|
119
|
+
offset += len(market_response.data)
|
|
120
|
+
remaining -= len(markets_to_add)
|
|
104
121
|
|
|
105
|
-
if
|
|
106
|
-
|
|
122
|
+
# Stop if we've reached our limit or there are no more results
|
|
123
|
+
if (
|
|
124
|
+
remaining <= 0
|
|
125
|
+
or not market_response.pagination.hasMore
|
|
126
|
+
or len(market_response.data) == 0
|
|
127
|
+
):
|
|
107
128
|
break
|
|
108
129
|
|
|
130
|
+
# Return exactly the number of items requested (in case we got more due to batch size)
|
|
109
131
|
return all_markets[:limit]
|
|
110
132
|
|
|
111
133
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
@tenacity.retry(
|
|
135
|
+
stop=tenacity.stop_after_attempt(2),
|
|
136
|
+
wait=tenacity.wait_fixed(1),
|
|
137
|
+
after=lambda x: logger.debug(
|
|
138
|
+
f"get_user_positions failed, attempt={x.attempt_number}."
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
def get_user_positions(
|
|
142
|
+
user_id: ChecksumAddress,
|
|
143
|
+
condition_ids: list[HexBytes] | None = None,
|
|
144
|
+
) -> list[PolymarketPositionResponse]:
|
|
145
|
+
"""Fetch a user's Polymarket positions; optionally filter by condition IDs."""
|
|
146
|
+
url = "https://data-api.polymarket.com/positions"
|
|
147
|
+
# ... rest of implementation ...
|
|
148
|
+
client: httpx.Client = HttpxCachedClient(ttl=timedelta(seconds=60)).get_client()
|
|
149
|
+
|
|
150
|
+
params = {
|
|
151
|
+
"user": user_id,
|
|
152
|
+
"market": ",".join([i.to_0x_hex() for i in condition_ids])
|
|
153
|
+
if condition_ids
|
|
154
|
+
else None,
|
|
155
|
+
"sortBy": "CASHPNL", # Available options: TOKENS, CURRENT, INITIAL, CASHPNL, PERCENTPNL, TITLE, RESOLVING, PRICE
|
|
156
|
+
}
|
|
157
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
token_id=token.token_id,
|
|
131
|
-
outcome=token.outcome,
|
|
132
|
-
winner=token.winner,
|
|
133
|
-
prices=Prices(
|
|
134
|
-
BUY=get_token_price(token.token_id, "buy").price_dec,
|
|
135
|
-
SELL=get_token_price(token.token_id, "sell").price_dec,
|
|
136
|
-
),
|
|
137
|
-
)
|
|
138
|
-
for token in market.tokens
|
|
139
|
-
]
|
|
140
|
-
return tokens_with_prices
|
|
159
|
+
response = client.get(url, params=params)
|
|
160
|
+
response.raise_for_status()
|
|
161
|
+
data = response.json()
|
|
162
|
+
items = [PolymarketPositionResponse.model_validate(d) for d in data]
|
|
163
|
+
return items
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from py_clob_client.client import ClobClient
|
|
5
|
+
from py_clob_client.clob_types import MarketOrderArgs, OrderType
|
|
6
|
+
from py_clob_client.order_builder.constants import BUY, SELL
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from web3 import Web3
|
|
9
|
+
|
|
10
|
+
from prediction_market_agent_tooling.chains import POLYGON_CHAIN_ID
|
|
11
|
+
from prediction_market_agent_tooling.config import APIKeys, RPCConfig
|
|
12
|
+
from prediction_market_agent_tooling.gtypes import USD, HexBytes, OutcomeToken, Wei
|
|
13
|
+
from prediction_market_agent_tooling.loggers import logger
|
|
14
|
+
from prediction_market_agent_tooling.markets.polymarket.constants import (
|
|
15
|
+
CTF_EXCHANGE_POLYMARKET,
|
|
16
|
+
NEG_RISK_ADAPTER,
|
|
17
|
+
NEG_RISK_EXCHANGE,
|
|
18
|
+
POLYMARKET_TINY_BET_AMOUNT,
|
|
19
|
+
)
|
|
20
|
+
from prediction_market_agent_tooling.markets.polymarket.polymarket_contracts import (
|
|
21
|
+
PolymarketConditionalTokenContract,
|
|
22
|
+
USDCeContract,
|
|
23
|
+
)
|
|
24
|
+
from prediction_market_agent_tooling.tools.cow.cow_order import handle_allowance
|
|
25
|
+
|
|
26
|
+
HOST = "https://clob.polymarket.com"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AllowanceResult(BaseModel):
|
|
30
|
+
balance: float
|
|
31
|
+
allowances: Dict[str, float]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PolymarketPriceSideEnum(str, Enum):
|
|
35
|
+
BUY = "BUY"
|
|
36
|
+
SELL = "SELL"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OrderStatusEnum(str, Enum):
|
|
40
|
+
MATCHED = "matched"
|
|
41
|
+
LIVE = "live"
|
|
42
|
+
DELAYED = "delayed"
|
|
43
|
+
UNMATCHED = "unmatched"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CreateOrderResult(BaseModel):
|
|
47
|
+
errorMsg: str
|
|
48
|
+
orderID: str
|
|
49
|
+
transactionsHashes: list[HexBytes]
|
|
50
|
+
status: OrderStatusEnum
|
|
51
|
+
success: bool
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PriceResponse(BaseModel):
|
|
55
|
+
price: float
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ClobManager:
|
|
59
|
+
def __init__(self, api_keys: APIKeys):
|
|
60
|
+
self.api_keys = api_keys
|
|
61
|
+
self.clob_client = ClobClient(
|
|
62
|
+
HOST,
|
|
63
|
+
key=api_keys.bet_from_private_key.get_secret_value(),
|
|
64
|
+
chain_id=POLYGON_CHAIN_ID,
|
|
65
|
+
)
|
|
66
|
+
self.clob_client.set_api_creds(self.clob_client.create_or_derive_api_creds())
|
|
67
|
+
self.polygon_web3 = RPCConfig().get_polygon_web3()
|
|
68
|
+
self.__init_approvals(polygon_web3=self.polygon_web3)
|
|
69
|
+
|
|
70
|
+
def get_token_price(self, token_id: int, side: PolymarketPriceSideEnum) -> USD:
|
|
71
|
+
price_data = self.clob_client.get_price(token_id=token_id, side=side.value)
|
|
72
|
+
price_item = PriceResponse.model_validate(price_data)
|
|
73
|
+
return USD(price_item.price)
|
|
74
|
+
|
|
75
|
+
def _place_market_order(
|
|
76
|
+
self, token_id: int, amount: float, side: PolymarketPriceSideEnum
|
|
77
|
+
) -> CreateOrderResult:
|
|
78
|
+
"""Internal method to place a market order.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
token_id: The token ID to trade
|
|
82
|
+
amount: The amount to trade (USDC for BUY, token shares for SELL)
|
|
83
|
+
side: Either BUY or SELL
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
CreateOrderResult: The result of the order placement
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If usdc_amount is < 1.0 for BUY orders
|
|
90
|
+
"""
|
|
91
|
+
if side == PolymarketPriceSideEnum.BUY and amount < 1.0:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"usdc_amounts < 1.0 are not supported by Polymarket, got {amount}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# We check allowances first
|
|
97
|
+
self.__init_approvals()
|
|
98
|
+
|
|
99
|
+
order_args = MarketOrderArgs(
|
|
100
|
+
token_id=str(token_id),
|
|
101
|
+
amount=amount,
|
|
102
|
+
side=side.value,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
logger.info(f"Placing market order: {order_args}")
|
|
106
|
+
signed_order = self.clob_client.create_market_order(order_args)
|
|
107
|
+
resp = self.clob_client.post_order(signed_order, orderType=OrderType.FOK)
|
|
108
|
+
return CreateOrderResult.model_validate(resp)
|
|
109
|
+
|
|
110
|
+
def place_buy_market_order(
|
|
111
|
+
self, token_id: int, usdc_amount: USD
|
|
112
|
+
) -> CreateOrderResult:
|
|
113
|
+
"""Place a market buy order for the given token with the specified USDC amount."""
|
|
114
|
+
return self._place_market_order(token_id, usdc_amount.value, BUY)
|
|
115
|
+
|
|
116
|
+
def place_sell_market_order(
|
|
117
|
+
self, token_id: int, token_shares: OutcomeToken
|
|
118
|
+
) -> CreateOrderResult:
|
|
119
|
+
"""Place a market sell order for the given token with the specified number of shares."""
|
|
120
|
+
return self._place_market_order(token_id, token_shares.value, SELL)
|
|
121
|
+
|
|
122
|
+
def __init_approvals(
|
|
123
|
+
self,
|
|
124
|
+
polygon_web3: Web3 | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
# from https://github.com/Polymarket/agents/blob/main/agents/polymarket/polymarket.py#L341
|
|
127
|
+
polygon_web3 = polygon_web3 or self.polygon_web3
|
|
128
|
+
|
|
129
|
+
usdc = USDCeContract()
|
|
130
|
+
|
|
131
|
+
# When setting allowances on Polymarket, it's important to set a large amount, because
|
|
132
|
+
# every trade reduces the allowance by the amount of the trade.
|
|
133
|
+
large_amount_wei = Wei(int(100 * 1e6)) # 100 USDC in Wei
|
|
134
|
+
amount_to_check_wei = Wei(int(POLYMARKET_TINY_BET_AMOUNT.value * 1e6))
|
|
135
|
+
ctf = PolymarketConditionalTokenContract()
|
|
136
|
+
|
|
137
|
+
for target_address in [
|
|
138
|
+
CTF_EXCHANGE_POLYMARKET,
|
|
139
|
+
NEG_RISK_EXCHANGE,
|
|
140
|
+
NEG_RISK_ADAPTER,
|
|
141
|
+
]:
|
|
142
|
+
logger.info(f"Checking allowances for {target_address}")
|
|
143
|
+
handle_allowance(
|
|
144
|
+
api_keys=self.api_keys,
|
|
145
|
+
sell_token=usdc.address,
|
|
146
|
+
for_address=target_address,
|
|
147
|
+
amount_to_check_wei=amount_to_check_wei,
|
|
148
|
+
amount_to_set_wei=large_amount_wei,
|
|
149
|
+
web3=polygon_web3,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
ctf.approve_if_not_approved(
|
|
153
|
+
api_keys=self.api_keys,
|
|
154
|
+
for_address=target_address,
|
|
155
|
+
web3=polygon_web3,
|
|
156
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from web3 import Web3
|
|
2
|
+
|
|
3
|
+
from prediction_market_agent_tooling.gtypes import USD
|
|
4
|
+
|
|
5
|
+
CTF_EXCHANGE_POLYMARKET = Web3.to_checksum_address(
|
|
6
|
+
"0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e"
|
|
7
|
+
)
|
|
8
|
+
NEG_RISK_EXCHANGE = Web3.to_checksum_address(
|
|
9
|
+
"0xC5d563A36AE78145C45a50134d48A1215220f80a"
|
|
10
|
+
)
|
|
11
|
+
NEG_RISK_ADAPTER = Web3.to_checksum_address(
|
|
12
|
+
"0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
|
|
13
|
+
)
|
|
14
|
+
# We reference this value in multiple files
|
|
15
|
+
POLYMARKET_TINY_BET_AMOUNT = USD(1.0)
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
1
3
|
from pydantic import BaseModel
|
|
2
4
|
|
|
3
5
|
from prediction_market_agent_tooling.gtypes import USDC, OutcomeStr, Probability
|
|
4
6
|
from prediction_market_agent_tooling.markets.data_models import Resolution
|
|
5
|
-
from prediction_market_agent_tooling.
|
|
6
|
-
POLYMARKET_FALSE_OUTCOME,
|
|
7
|
-
POLYMARKET_TRUE_OUTCOME,
|
|
8
|
-
PolymarketFullMarket,
|
|
9
|
-
construct_polymarket_url,
|
|
10
|
-
)
|
|
7
|
+
from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
|
|
11
8
|
from prediction_market_agent_tooling.tools.utils import DatetimeUTC
|
|
12
9
|
|
|
10
|
+
POLYMARKET_TRUE_OUTCOME = "Yes"
|
|
11
|
+
POLYMARKET_FALSE_OUTCOME = "No"
|
|
12
|
+
|
|
13
|
+
POLYMARKET_BASE_URL = "https://polymarket.com"
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
class PolymarketRewards(BaseModel):
|
|
15
17
|
min_size: int
|
|
@@ -26,6 +28,72 @@ class PolymarketToken(BaseModel):
|
|
|
26
28
|
winner: bool
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
class PolymarketGammaMarket(BaseModel):
|
|
32
|
+
conditionId: HexBytes
|
|
33
|
+
outcomes: str
|
|
34
|
+
outcomePrices: str | None = None
|
|
35
|
+
marketMakerAddress: str
|
|
36
|
+
createdAt: DatetimeUTC
|
|
37
|
+
updatedAt: DatetimeUTC | None = None
|
|
38
|
+
archived: bool
|
|
39
|
+
questionId: str | None = None
|
|
40
|
+
clobTokenIds: str | None = None # int-encoded hex
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def token_ids(self) -> list[int]:
|
|
44
|
+
# If market has no token_ids, we halt for safety since it will fail later on.
|
|
45
|
+
if not self.clobTokenIds:
|
|
46
|
+
raise ValueError("Market has no token_ids")
|
|
47
|
+
return [int(i) for i in json.loads(self.clobTokenIds)]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def outcomes_list(self) -> list[OutcomeStr]:
|
|
51
|
+
return [OutcomeStr(i) for i in json.loads(self.outcomes)]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def outcome_prices(self) -> list[float] | None:
|
|
55
|
+
if not self.outcomePrices:
|
|
56
|
+
return None
|
|
57
|
+
return [float(i) for i in json.loads(self.outcomePrices)]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PolymarketGammaTag(BaseModel):
|
|
61
|
+
label: str
|
|
62
|
+
slug: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PolymarketGammaResponseDataItem(BaseModel):
|
|
66
|
+
id: str
|
|
67
|
+
slug: str
|
|
68
|
+
volume: float | None = None
|
|
69
|
+
startDate: DatetimeUTC | None = None
|
|
70
|
+
endDate: DatetimeUTC | None = None
|
|
71
|
+
liquidity: float | None = None
|
|
72
|
+
liquidityClob: float | None = None
|
|
73
|
+
title: str
|
|
74
|
+
description: str | None = None
|
|
75
|
+
archived: bool
|
|
76
|
+
closed: bool
|
|
77
|
+
active: bool
|
|
78
|
+
markets: list[
|
|
79
|
+
PolymarketGammaMarket
|
|
80
|
+
] | None = None # Some Polymarket markets have missing markets field. We skip these markets manually when retrieving.
|
|
81
|
+
tags: list[PolymarketGammaTag]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def url(self) -> str:
|
|
85
|
+
return construct_polymarket_url(self.slug)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PolymarketGammaPagination(BaseModel):
|
|
89
|
+
hasMore: bool
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PolymarketGammaResponse(BaseModel):
|
|
93
|
+
data: list[PolymarketGammaResponseDataItem]
|
|
94
|
+
pagination: PolymarketGammaPagination
|
|
95
|
+
|
|
96
|
+
|
|
29
97
|
class PolymarketMarket(BaseModel):
|
|
30
98
|
enable_order_book: bool
|
|
31
99
|
active: bool
|
|
@@ -89,19 +157,6 @@ class PolymarketMarket(BaseModel):
|
|
|
89
157
|
f"Should not happen, invalid winner tokens: {winner_tokens}"
|
|
90
158
|
)
|
|
91
159
|
|
|
92
|
-
def fetch_full_market(self) -> PolymarketFullMarket | None:
|
|
93
|
-
return PolymarketFullMarket.fetch_from_url(self.url)
|
|
94
|
-
|
|
95
|
-
def fetch_if_its_a_main_market(self) -> bool:
|
|
96
|
-
# On Polymarket, there are markets that are actually a group of multiple Yes/No markets, for example https://polymarket.com/event/presidential-election-winner-2024.
|
|
97
|
-
# But API returns them individually, and then we receive questions such as "Will any other Republican Politician win the 2024 US Presidential Election?",
|
|
98
|
-
# which are naturally unpredictable without futher details.
|
|
99
|
-
# This is a heuristic to filter them out.
|
|
100
|
-
# Warning: This is a very slow operation, as it requires fetching the website. Use it only when necessary.
|
|
101
|
-
full_market = self.fetch_full_market()
|
|
102
|
-
# `full_market` can be None, if this class come from a multiple Yes/No market, becase then, the constructed URL is invalid (and there is now way to construct an valid one from the data we have).
|
|
103
|
-
return full_market is not None and full_market.is_main_market
|
|
104
|
-
|
|
105
160
|
|
|
106
161
|
class MarketsEndpointResponse(BaseModel):
|
|
107
162
|
limit: int
|
|
@@ -138,3 +193,24 @@ class PolymarketMarketWithPrices(PolymarketMarket):
|
|
|
138
193
|
raise ValueError(
|
|
139
194
|
"Should not happen, as we filter only for binary markets in get_polymarket_binary_markets."
|
|
140
195
|
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PolymarketPositionResponse(BaseModel):
|
|
199
|
+
slug: str
|
|
200
|
+
eventSlug: str
|
|
201
|
+
proxyWallet: str
|
|
202
|
+
asset: str
|
|
203
|
+
conditionId: str
|
|
204
|
+
size: float
|
|
205
|
+
currentValue: float
|
|
206
|
+
cashPnl: float
|
|
207
|
+
redeemable: bool
|
|
208
|
+
outcome: str
|
|
209
|
+
outcomeIndex: int
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def construct_polymarket_url(slug: str) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Note: This works only if it's a single main market, not sub-market of some more general question.
|
|
215
|
+
"""
|
|
216
|
+
return f"{POLYMARKET_BASE_URL}/event/{slug}"
|