prediction-market-agent-tooling 0.66.5__py3-none-any.whl → 0.66.6__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/deploy/agent.py +23 -5
- prediction_market_agent_tooling/markets/agent_market.py +9 -2
- prediction_market_agent_tooling/markets/manifold/manifold.py +3 -2
- prediction_market_agent_tooling/markets/markets.py +5 -5
- prediction_market_agent_tooling/markets/metaculus/metaculus.py +4 -2
- prediction_market_agent_tooling/markets/omen/omen.py +5 -2
- prediction_market_agent_tooling/markets/polymarket/api.py +87 -104
- prediction_market_agent_tooling/markets/polymarket/data_models.py +60 -14
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -54
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +109 -26
- prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +49 -0
- prediction_market_agent_tooling/markets/polymarket/utils.py +0 -21
- prediction_market_agent_tooling/markets/seer/seer.py +14 -18
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +48 -35
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +10 -0
- prediction_market_agent_tooling/tools/cow/cow_order.py +2 -0
- prediction_market_agent_tooling/tools/httpx_cached_client.py +13 -6
- prediction_market_agent_tooling/tools/tokens/auto_deposit.py +7 -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/utils.py +5 -2
- {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/METADATA +1 -1
- {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/RECORD +26 -24
- {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/LICENSE +0 -0
- {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/WHEEL +0 -0
- {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/entry_points.txt +0 -0
@@ -19,9 +19,11 @@ from prediction_market_agent_tooling.deploy.trade_interval import (
|
|
19
19
|
)
|
20
20
|
from prediction_market_agent_tooling.gtypes import USD, OutcomeToken, xDai
|
21
21
|
from prediction_market_agent_tooling.loggers import logger
|
22
|
+
from prediction_market_agent_tooling.markets.agent_market import AgentMarket, FilterBy
|
23
|
+
from prediction_market_agent_tooling.markets.agent_market import (
|
24
|
+
MarketType as AgentMarketType,
|
25
|
+
)
|
22
26
|
from prediction_market_agent_tooling.markets.agent_market import (
|
23
|
-
AgentMarket,
|
24
|
-
FilterBy,
|
25
27
|
ProcessedMarket,
|
26
28
|
ProcessedTradedMarket,
|
27
29
|
SortBy,
|
@@ -335,6 +337,15 @@ class DeployablePredictionAgent(DeployableAgent):
|
|
335
337
|
return True
|
336
338
|
return False
|
337
339
|
|
340
|
+
@property
|
341
|
+
def agent_market_type(self) -> AgentMarketType:
|
342
|
+
if self.fetch_scalar_markets:
|
343
|
+
return AgentMarketType.SCALAR
|
344
|
+
elif self.fetch_categorical_markets:
|
345
|
+
return AgentMarketType.CATEGORICAL
|
346
|
+
else:
|
347
|
+
return AgentMarketType.BINARY
|
348
|
+
|
338
349
|
def get_markets(
|
339
350
|
self,
|
340
351
|
market_type: MarketType,
|
@@ -343,14 +354,16 @@ class DeployablePredictionAgent(DeployableAgent):
|
|
343
354
|
Override this method to customize what markets will fetch for processing.
|
344
355
|
"""
|
345
356
|
cls = market_type.market_class
|
357
|
+
|
358
|
+
agent_market_type = self.agent_market_type
|
359
|
+
|
346
360
|
# Fetch the soonest closing markets to choose from
|
347
361
|
available_markets = cls.get_markets(
|
348
362
|
limit=self.n_markets_to_fetch,
|
349
363
|
sort_by=self.get_markets_sort_by,
|
350
364
|
filter_by=self.get_markets_filter_by,
|
351
365
|
created_after=self.trade_on_markets_created_after,
|
352
|
-
|
353
|
-
fetch_scalar_markets=self.fetch_scalar_markets,
|
366
|
+
market_type=agent_market_type,
|
354
367
|
)
|
355
368
|
return available_markets
|
356
369
|
|
@@ -628,7 +641,12 @@ class DeployableTraderAgent(DeployablePredictionAgent):
|
|
628
641
|
api_keys = APIKeys()
|
629
642
|
user_id = market.get_user_id(api_keys=api_keys)
|
630
643
|
|
631
|
-
|
644
|
+
try:
|
645
|
+
existing_position = market.get_position(user_id=user_id)
|
646
|
+
except Exception as e:
|
647
|
+
logger.warning(f"Could not get position for user {user_id}, exception {e}")
|
648
|
+
return None
|
649
|
+
|
632
650
|
trades = self.build_trades(
|
633
651
|
market=market,
|
634
652
|
answer=processed_market.answer,
|
@@ -64,6 +64,13 @@ class FilterBy(str, Enum):
|
|
64
64
|
NONE = "none"
|
65
65
|
|
66
66
|
|
67
|
+
class MarketType(str, Enum):
|
68
|
+
ALL = "all"
|
69
|
+
CATEGORICAL = "categorical"
|
70
|
+
SCALAR = "scalar"
|
71
|
+
BINARY = "binary"
|
72
|
+
|
73
|
+
|
67
74
|
class AgentMarket(BaseModel):
|
68
75
|
"""
|
69
76
|
Common market class that can be created from vendor specific markets.
|
@@ -369,8 +376,8 @@ class AgentMarket(BaseModel):
|
|
369
376
|
filter_by: FilterBy = FilterBy.OPEN,
|
370
377
|
created_after: t.Optional[DatetimeUTC] = None,
|
371
378
|
excluded_questions: set[str] | None = None,
|
372
|
-
|
373
|
-
|
379
|
+
market_type: MarketType = MarketType.ALL,
|
380
|
+
include_conditional_markets: bool = False,
|
374
381
|
) -> t.Sequence["AgentMarket"]:
|
375
382
|
raise NotImplementedError("Subclasses must implement this method")
|
376
383
|
|
@@ -12,6 +12,7 @@ from prediction_market_agent_tooling.markets.agent_market import (
|
|
12
12
|
AgentMarket,
|
13
13
|
FilterBy,
|
14
14
|
MarketFees,
|
15
|
+
MarketType,
|
15
16
|
SortBy,
|
16
17
|
)
|
17
18
|
from prediction_market_agent_tooling.markets.manifold.api import (
|
@@ -110,8 +111,8 @@ class ManifoldAgentMarket(AgentMarket):
|
|
110
111
|
filter_by: FilterBy = FilterBy.OPEN,
|
111
112
|
created_after: t.Optional[DatetimeUTC] = None,
|
112
113
|
excluded_questions: set[str] | None = None,
|
113
|
-
|
114
|
-
|
114
|
+
market_type: MarketType = MarketType.ALL,
|
115
|
+
include_conditional_markets: bool = False,
|
115
116
|
) -> t.Sequence["ManifoldAgentMarket"]:
|
116
117
|
sort: t.Literal["newest", "close-date"] | None
|
117
118
|
if sort_by == SortBy.CLOSING_SOONEST:
|
@@ -3,11 +3,11 @@ from enum import Enum
|
|
3
3
|
|
4
4
|
from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
|
5
5
|
from prediction_market_agent_tooling.jobs.omen.omen_jobs import OmenJobAgentMarket
|
6
|
+
from prediction_market_agent_tooling.markets.agent_market import AgentMarket, FilterBy
|
6
7
|
from prediction_market_agent_tooling.markets.agent_market import (
|
7
|
-
|
8
|
-
FilterBy,
|
9
|
-
SortBy,
|
8
|
+
MarketType as AgentMarketType,
|
10
9
|
)
|
10
|
+
from prediction_market_agent_tooling.markets.agent_market import SortBy
|
11
11
|
from prediction_market_agent_tooling.markets.manifold.manifold import (
|
12
12
|
ManifoldAgentMarket,
|
13
13
|
)
|
@@ -68,7 +68,7 @@ def get_binary_markets(
|
|
68
68
|
sort_by: SortBy = SortBy.NONE,
|
69
69
|
excluded_questions: set[str] | None = None,
|
70
70
|
created_after: DatetimeUTC | None = None,
|
71
|
-
|
71
|
+
agent_market_type: AgentMarketType = AgentMarketType.BINARY,
|
72
72
|
) -> t.Sequence[AgentMarket]:
|
73
73
|
agent_market_class = MARKET_TYPE_TO_AGENT_MARKET[market_type]
|
74
74
|
markets = agent_market_class.get_markets(
|
@@ -77,6 +77,6 @@ def get_binary_markets(
|
|
77
77
|
filter_by=filter_by,
|
78
78
|
created_after=created_after,
|
79
79
|
excluded_questions=excluded_questions,
|
80
|
-
|
80
|
+
market_type=agent_market_type,
|
81
81
|
)
|
82
82
|
return markets
|
@@ -9,6 +9,7 @@ from prediction_market_agent_tooling.markets.agent_market import (
|
|
9
9
|
AgentMarket,
|
10
10
|
FilterBy,
|
11
11
|
MarketFees,
|
12
|
+
MarketType,
|
12
13
|
ProcessedMarket,
|
13
14
|
SortBy,
|
14
15
|
)
|
@@ -66,14 +67,15 @@ class MetaculusAgentMarket(AgentMarket):
|
|
66
67
|
)
|
67
68
|
|
68
69
|
@staticmethod
|
69
|
-
def get_markets(
|
70
|
+
def get_markets( # type: ignore[override]
|
70
71
|
limit: int,
|
71
72
|
sort_by: SortBy = SortBy.NONE,
|
72
73
|
filter_by: FilterBy = FilterBy.OPEN,
|
73
74
|
created_after: t.Optional[DatetimeUTC] = None,
|
74
75
|
excluded_questions: set[str] | None = None,
|
75
76
|
tournament_id: int | None = None,
|
76
|
-
|
77
|
+
market_type: MarketType = MarketType.ALL,
|
78
|
+
include_conditional_markets: bool = False,
|
77
79
|
) -> t.Sequence["MetaculusAgentMarket"]:
|
78
80
|
order_by: str | None
|
79
81
|
if sort_by == SortBy.NONE:
|
@@ -25,6 +25,7 @@ from prediction_market_agent_tooling.markets.agent_market import (
|
|
25
25
|
AgentMarket,
|
26
26
|
FilterBy,
|
27
27
|
MarketFees,
|
28
|
+
MarketType,
|
28
29
|
ProcessedMarket,
|
29
30
|
ProcessedTradedMarket,
|
30
31
|
SortBy,
|
@@ -379,9 +380,11 @@ class OmenAgentMarket(AgentMarket):
|
|
379
380
|
filter_by: FilterBy = FilterBy.OPEN,
|
380
381
|
created_after: t.Optional[DatetimeUTC] = None,
|
381
382
|
excluded_questions: set[str] | None = None,
|
382
|
-
|
383
|
-
|
383
|
+
market_type: MarketType = MarketType.ALL,
|
384
|
+
include_conditional_markets: bool = False,
|
384
385
|
) -> t.Sequence["OmenAgentMarket"]:
|
386
|
+
fetch_categorical_markets = market_type == MarketType.CATEGORICAL
|
387
|
+
|
385
388
|
return [
|
386
389
|
OmenAgentMarket.from_data_model(m)
|
387
390
|
for m in OmenSubgraphHandler().get_omen_markets_simple(
|
@@ -1,140 +1,123 @@
|
|
1
1
|
import typing as t
|
2
|
+
from enum import Enum
|
3
|
+
from urllib.parse import urljoin
|
2
4
|
|
3
|
-
import
|
5
|
+
import httpx
|
4
6
|
import tenacity
|
5
7
|
|
6
8
|
from prediction_market_agent_tooling.loggers import logger
|
7
9
|
from prediction_market_agent_tooling.markets.polymarket.data_models import (
|
8
10
|
POLYMARKET_FALSE_OUTCOME,
|
9
11
|
POLYMARKET_TRUE_OUTCOME,
|
10
|
-
|
11
|
-
|
12
|
-
PolymarketMarketWithPrices,
|
13
|
-
PolymarketPriceResponse,
|
14
|
-
PolymarketTokenWithPrices,
|
15
|
-
Prices,
|
12
|
+
PolymarketGammaResponse,
|
13
|
+
PolymarketGammaResponseDataItem,
|
16
14
|
)
|
15
|
+
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
16
|
+
from prediction_market_agent_tooling.tools.httpx_cached_client import HttpxCachedClient
|
17
17
|
from prediction_market_agent_tooling.tools.utils import response_to_model
|
18
18
|
|
19
|
-
POLYMARKET_API_BASE_URL = "https://clob.polymarket.com/"
|
20
19
|
MARKETS_LIMIT = 100 # Polymarket will only return up to 100 markets
|
20
|
+
POLYMARKET_GAMMA_API_BASE_URL = "https://gamma-api.polymarket.com/"
|
21
|
+
|
22
|
+
|
23
|
+
class PolymarketOrderByEnum(str, Enum):
|
24
|
+
LIQUIDITY = "liquidity"
|
25
|
+
START_DATE = "startDate"
|
26
|
+
END_DATE = "endDate"
|
27
|
+
VOLUME_24HR = "volume24hr"
|
21
28
|
|
22
29
|
|
23
30
|
@tenacity.retry(
|
24
|
-
stop=tenacity.stop_after_attempt(
|
25
|
-
wait=tenacity.
|
26
|
-
after=lambda x: logger.debug(
|
31
|
+
stop=tenacity.stop_after_attempt(2),
|
32
|
+
wait=tenacity.wait_fixed(1),
|
33
|
+
after=lambda x: logger.debug(
|
34
|
+
f"get_polymarkets_with_pagination failed, {x.attempt_number=}."
|
35
|
+
),
|
27
36
|
)
|
28
|
-
def
|
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)
|
42
|
-
|
43
|
-
|
44
|
-
def get_polymarket_binary_markets(
|
37
|
+
def get_polymarkets_with_pagination(
|
45
38
|
limit: int,
|
46
|
-
|
39
|
+
created_after: t.Optional[DatetimeUTC] = None,
|
40
|
+
active: bool | None = None,
|
41
|
+
closed: bool | None = None,
|
47
42
|
excluded_questions: set[str] | None = None,
|
48
|
-
|
49
|
-
|
50
|
-
|
43
|
+
only_binary: bool = True,
|
44
|
+
archived: bool = False,
|
45
|
+
ascending: bool = False,
|
46
|
+
order_by: PolymarketOrderByEnum = PolymarketOrderByEnum.VOLUME_24HR,
|
47
|
+
) -> list[PolymarketGammaResponseDataItem]:
|
51
48
|
"""
|
52
|
-
|
49
|
+
Binary markets have len(model.markets) == 1.
|
50
|
+
Categorical markets have len(model.markets) > 1
|
53
51
|
"""
|
54
|
-
|
55
|
-
all_markets: list[
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
52
|
+
client: httpx.Client = HttpxCachedClient(ttl=60).get_client()
|
53
|
+
all_markets: list[PolymarketGammaResponseDataItem] = []
|
54
|
+
offset = 0
|
55
|
+
remaining = limit
|
56
|
+
|
57
|
+
while remaining > 0:
|
58
|
+
# Calculate how many items to request in this batch (up to MARKETS_LIMIT or remaining)
|
59
|
+
# By default we fetch many markets because not possible to filter by binary/categorical
|
60
|
+
batch_size = MARKETS_LIMIT
|
61
|
+
|
62
|
+
# Build query parameters, excluding None values
|
63
|
+
params = {
|
64
|
+
"limit": batch_size,
|
65
|
+
"active": str(active).lower() if active is not None else None,
|
66
|
+
"archived": str(archived).lower(),
|
67
|
+
"closed": str(closed).lower() if closed is not None else None,
|
68
|
+
"order": order_by.value,
|
69
|
+
"ascending": str(ascending).lower(),
|
70
|
+
"offset": offset,
|
71
|
+
}
|
72
|
+
params_not_none = {k: v for k, v in params.items() if v is not None}
|
73
|
+
url = urljoin(
|
74
|
+
POLYMARKET_GAMMA_API_BASE_URL,
|
75
|
+
f"events/pagination",
|
61
76
|
)
|
62
77
|
|
63
|
-
|
64
|
-
|
65
|
-
if closed is not None and market.closed != closed:
|
66
|
-
continue
|
67
|
-
|
68
|
-
# Skip markets that are inactive.
|
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
|
78
|
+
r = client.get(url, params=params_not_none)
|
79
|
+
r.raise_for_status()
|
72
80
|
|
73
|
-
|
74
|
-
# Again nothing about it in documentation and API doesn't seem to return them, but to be safe.
|
75
|
-
if market.archived:
|
76
|
-
continue
|
81
|
+
market_response = response_to_model(r, PolymarketGammaResponse)
|
77
82
|
|
78
|
-
|
83
|
+
markets_to_add = []
|
84
|
+
for m in market_response.data:
|
85
|
+
if excluded_questions and m.title in excluded_questions:
|
79
86
|
continue
|
80
87
|
|
81
|
-
|
82
|
-
if
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
88
|
+
sorted_outcome_list = sorted(m.markets[0].outcomes_list)
|
89
|
+
if only_binary:
|
90
|
+
# We keep markets that are only Yes,No
|
91
|
+
if len(m.markets) > 1 or sorted_outcome_list != [
|
92
|
+
POLYMARKET_FALSE_OUTCOME,
|
93
|
+
POLYMARKET_TRUE_OUTCOME,
|
94
|
+
]:
|
95
|
+
continue
|
87
96
|
|
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():
|
97
|
+
if created_after and created_after > m.startDate:
|
91
98
|
continue
|
92
99
|
|
93
|
-
|
94
|
-
market_with_prices = PolymarketMarketWithPrices.model_validate(
|
95
|
-
{**market.model_dump(), "tokens": tokens_with_price}
|
96
|
-
)
|
100
|
+
markets_to_add.append(m)
|
97
101
|
|
98
|
-
|
102
|
+
if only_binary:
|
103
|
+
markets_to_add = [
|
104
|
+
market for market in market_response.data if len(market.markets) == 1
|
105
|
+
]
|
99
106
|
|
100
|
-
|
101
|
-
|
107
|
+
# Add the markets from this batch to our results
|
108
|
+
all_markets.extend(markets_to_add)
|
102
109
|
|
103
|
-
|
110
|
+
# Update counters
|
111
|
+
offset += len(market_response.data)
|
112
|
+
remaining -= len(markets_to_add)
|
104
113
|
|
105
|
-
if
|
106
|
-
|
114
|
+
# Stop if we've reached our limit or there are no more results
|
115
|
+
if (
|
116
|
+
remaining <= 0
|
117
|
+
or not market_response.pagination.hasMore
|
118
|
+
or len(market_response.data) == 0
|
119
|
+
):
|
107
120
|
break
|
108
121
|
|
122
|
+
# Return exactly the number of items requested (in case we got more due to batch size)
|
109
123
|
return all_markets[:limit]
|
110
|
-
|
111
|
-
|
112
|
-
def get_polymarket_market(condition_id: str) -> PolymarketMarket:
|
113
|
-
url = f"{POLYMARKET_API_BASE_URL}/markets/{condition_id}"
|
114
|
-
return response_to_model(requests.get(url), PolymarketMarket)
|
115
|
-
|
116
|
-
|
117
|
-
def get_token_price(
|
118
|
-
token_id: str, side: t.Literal["buy", "sell"]
|
119
|
-
) -> PolymarketPriceResponse:
|
120
|
-
url = f"{POLYMARKET_API_BASE_URL}/price"
|
121
|
-
params = {"token_id": token_id, "side": side}
|
122
|
-
return response_to_model(requests.get(url, params=params), PolymarketPriceResponse)
|
123
|
-
|
124
|
-
|
125
|
-
def get_market_tokens_with_prices(
|
126
|
-
market: PolymarketMarket,
|
127
|
-
) -> list[PolymarketTokenWithPrices]:
|
128
|
-
tokens_with_prices = [
|
129
|
-
PolymarketTokenWithPrices(
|
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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import json
|
2
|
+
|
1
3
|
from pydantic import BaseModel
|
2
4
|
|
3
5
|
from prediction_market_agent_tooling.gtypes import USDC, OutcomeStr, Probability
|
@@ -5,9 +7,9 @@ from prediction_market_agent_tooling.markets.data_models import Resolution
|
|
5
7
|
from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
|
6
8
|
POLYMARKET_FALSE_OUTCOME,
|
7
9
|
POLYMARKET_TRUE_OUTCOME,
|
8
|
-
PolymarketFullMarket,
|
9
10
|
construct_polymarket_url,
|
10
11
|
)
|
12
|
+
from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
|
11
13
|
from prediction_market_agent_tooling.tools.utils import DatetimeUTC
|
12
14
|
|
13
15
|
|
@@ -26,6 +28,63 @@ 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 outcomes_list(self) -> list[OutcomeStr]:
|
44
|
+
return [OutcomeStr(i) for i in json.loads(self.outcomes)]
|
45
|
+
|
46
|
+
@property
|
47
|
+
def outcome_prices(self) -> list[float] | None:
|
48
|
+
if not self.outcomePrices:
|
49
|
+
return None
|
50
|
+
return [float(i) for i in json.loads(self.outcomePrices)]
|
51
|
+
|
52
|
+
|
53
|
+
class PolymarketGammaTag(BaseModel):
|
54
|
+
label: str
|
55
|
+
slug: str
|
56
|
+
|
57
|
+
|
58
|
+
class PolymarketGammaResponseDataItem(BaseModel):
|
59
|
+
id: str
|
60
|
+
slug: str
|
61
|
+
volume: float | None = None
|
62
|
+
startDate: DatetimeUTC
|
63
|
+
endDate: DatetimeUTC | None = None
|
64
|
+
liquidity: float | None = None
|
65
|
+
liquidityClob: float | None = None
|
66
|
+
title: str
|
67
|
+
description: str
|
68
|
+
archived: bool
|
69
|
+
closed: bool
|
70
|
+
active: bool
|
71
|
+
markets: list[PolymarketGammaMarket]
|
72
|
+
tags: list[PolymarketGammaTag]
|
73
|
+
|
74
|
+
@property
|
75
|
+
def url(self) -> str:
|
76
|
+
return construct_polymarket_url(self.slug)
|
77
|
+
|
78
|
+
|
79
|
+
class PolymarketGammaPagination(BaseModel):
|
80
|
+
hasMore: bool
|
81
|
+
|
82
|
+
|
83
|
+
class PolymarketGammaResponse(BaseModel):
|
84
|
+
data: list[PolymarketGammaResponseDataItem]
|
85
|
+
pagination: PolymarketGammaPagination
|
86
|
+
|
87
|
+
|
29
88
|
class PolymarketMarket(BaseModel):
|
30
89
|
enable_order_book: bool
|
31
90
|
active: bool
|
@@ -89,19 +148,6 @@ class PolymarketMarket(BaseModel):
|
|
89
148
|
f"Should not happen, invalid winner tokens: {winner_tokens}"
|
90
149
|
)
|
91
150
|
|
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
151
|
|
106
152
|
class MarketsEndpointResponse(BaseModel):
|
107
153
|
limit: int
|
@@ -5,14 +5,11 @@ Keep in mind that not all fields were used so far, so there might be some bugs.
|
|
5
5
|
These models are based on what Polymarket's website returns in the response, and `prediction_market_agent_tooling/markets/polymarket/data_models.py` are based on what their API returns.
|
6
6
|
"""
|
7
7
|
|
8
|
-
import json
|
9
8
|
import typing as t
|
10
9
|
|
11
|
-
import requests
|
12
10
|
from pydantic import BaseModel, field_validator
|
13
11
|
|
14
12
|
from prediction_market_agent_tooling.gtypes import USDC, HexAddress, OutcomeStr
|
15
|
-
from prediction_market_agent_tooling.loggers import logger
|
16
13
|
from prediction_market_agent_tooling.markets.data_models import Resolution
|
17
14
|
from prediction_market_agent_tooling.tools.utils import DatetimeUTC
|
18
15
|
|
@@ -293,57 +290,6 @@ class PolymarketFullMarket(BaseModel):
|
|
293
290
|
)
|
294
291
|
return self.markets[0]
|
295
292
|
|
296
|
-
@staticmethod
|
297
|
-
def fetch_from_url(url: str) -> "PolymarketFullMarket | None":
|
298
|
-
"""
|
299
|
-
Get the full market data from the Polymarket website.
|
300
|
-
|
301
|
-
Returns None if this market's url returns "Oops...we didn't forecast this", see `check_if_its_a_main_market` method for more details.
|
302
|
-
|
303
|
-
Warning: This is a very slow operation, as it requires fetching the website. Use it only when necessary.
|
304
|
-
"""
|
305
|
-
logger.info(f"Fetching full market from {url}")
|
306
|
-
|
307
|
-
# Fetch the website as a normal browser would.
|
308
|
-
headers = {
|
309
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
310
|
-
}
|
311
|
-
content = requests.get(url, headers=headers).text
|
312
|
-
|
313
|
-
# Find the JSON with the data within the content.
|
314
|
-
start_tag = """<script id="__NEXT_DATA__" type="application/json" crossorigin="anonymous">"""
|
315
|
-
start_idx = content.find(start_tag) + len(start_tag)
|
316
|
-
end_idx = content.find("</script>", start_idx)
|
317
|
-
response_data = content[start_idx:end_idx]
|
318
|
-
|
319
|
-
# Parsing.
|
320
|
-
response_dict = json.loads(response_data)
|
321
|
-
response_model = PolymarketWebResponse.model_validate(response_dict)
|
322
|
-
|
323
|
-
full_market_queries = [
|
324
|
-
q
|
325
|
-
for q in response_model.props.pageProps.dehydratedState.queries
|
326
|
-
if isinstance(q.state.data, PolymarketFullMarket)
|
327
|
-
]
|
328
|
-
|
329
|
-
# We expect either 0 markets (if it doesn't exist) or 1 market.
|
330
|
-
if len(full_market_queries) not in (0, 1):
|
331
|
-
raise ValueError(
|
332
|
-
f"Unexpected number of queries in the response, please check it out and modify the code accordingly: `{response_dict}`"
|
333
|
-
)
|
334
|
-
|
335
|
-
# It will be `PolymarketFullMarket` thanks to the filter above.
|
336
|
-
market = (
|
337
|
-
t.cast(PolymarketFullMarket, full_market_queries[0].state.data)
|
338
|
-
if full_market_queries
|
339
|
-
else None
|
340
|
-
)
|
341
|
-
|
342
|
-
if market is None:
|
343
|
-
logger.warning(f"No polymarket found for {url}")
|
344
|
-
|
345
|
-
return market
|
346
|
-
|
347
293
|
|
348
294
|
class PriceSide(BaseModel):
|
349
295
|
price: USDC
|