prediction-market-agent-tooling 0.66.5__py3-none-any.whl → 0.67.1__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.
Files changed (28) hide show
  1. prediction_market_agent_tooling/deploy/agent.py +23 -5
  2. prediction_market_agent_tooling/markets/agent_market.py +9 -2
  3. prediction_market_agent_tooling/markets/manifold/manifold.py +3 -2
  4. prediction_market_agent_tooling/markets/markets.py +5 -5
  5. prediction_market_agent_tooling/markets/metaculus/metaculus.py +3 -1
  6. prediction_market_agent_tooling/markets/omen/omen.py +5 -2
  7. prediction_market_agent_tooling/markets/polymarket/api.py +87 -104
  8. prediction_market_agent_tooling/markets/polymarket/data_models.py +60 -14
  9. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -54
  10. prediction_market_agent_tooling/markets/polymarket/polymarket.py +109 -26
  11. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +49 -0
  12. prediction_market_agent_tooling/markets/polymarket/utils.py +0 -21
  13. prediction_market_agent_tooling/markets/seer/price_manager.py +65 -46
  14. prediction_market_agent_tooling/markets/seer/seer.py +70 -90
  15. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +48 -35
  16. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +11 -1
  17. prediction_market_agent_tooling/tools/cow/cow_order.py +26 -11
  18. prediction_market_agent_tooling/tools/cow/models.py +4 -2
  19. prediction_market_agent_tooling/tools/httpx_cached_client.py +13 -6
  20. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +7 -0
  21. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
  22. prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
  23. prediction_market_agent_tooling/tools/utils.py +5 -2
  24. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/METADATA +1 -1
  25. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/RECORD +28 -26
  26. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/LICENSE +0 -0
  27. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/WHEEL +0 -0
  28. {prediction_market_agent_tooling-0.66.5.dist-info → prediction_market_agent_tooling-0.67.1.dist-info}/entry_points.txt +0 -0
@@ -1,22 +1,35 @@
1
1
  import typing as t
2
2
 
3
- from prediction_market_agent_tooling.gtypes import USD, CollateralToken, OutcomeStr
3
+ from prediction_market_agent_tooling.gtypes import (
4
+ USD,
5
+ CollateralToken,
6
+ HexBytes,
7
+ OutcomeStr,
8
+ Probability,
9
+ )
4
10
  from prediction_market_agent_tooling.markets.agent_market import (
5
11
  AgentMarket,
6
12
  FilterBy,
7
13
  MarketFees,
14
+ MarketType,
8
15
  SortBy,
9
16
  )
17
+ from prediction_market_agent_tooling.markets.data_models import Resolution
10
18
  from prediction_market_agent_tooling.markets.polymarket.api import (
11
- get_polymarket_binary_markets,
19
+ PolymarketOrderByEnum,
20
+ get_polymarkets_with_pagination,
12
21
  )
13
22
  from prediction_market_agent_tooling.markets.polymarket.data_models import (
14
- PolymarketMarketWithPrices,
23
+ PolymarketGammaResponseDataItem,
15
24
  )
16
25
  from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
17
26
  POLYMARKET_BASE_URL,
18
27
  )
19
- from prediction_market_agent_tooling.tools.utils import DatetimeUTC
28
+ from prediction_market_agent_tooling.markets.polymarket.polymarket_subgraph_handler import (
29
+ ConditionSubgraphModel,
30
+ PolymarketSubgraphHandler,
31
+ )
32
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
20
33
 
21
34
 
22
35
  class PolymarketAgentMarket(AgentMarket):
@@ -33,19 +46,68 @@ class PolymarketAgentMarket(AgentMarket):
33
46
  fees: MarketFees = MarketFees.get_zero_fees()
34
47
 
35
48
  @staticmethod
36
- def from_data_model(model: PolymarketMarketWithPrices) -> "PolymarketAgentMarket":
49
+ def build_resolution_from_condition(
50
+ condition_id: HexBytes,
51
+ condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
52
+ outcomes: list[OutcomeStr],
53
+ ) -> Resolution | None:
54
+ condition_model = condition_model_dict.get(condition_id)
55
+ if (
56
+ not condition_model
57
+ or condition_model.resolutionTimestamp is None
58
+ or not condition_model.payoutNumerators
59
+ or not condition_model.payoutDenominator
60
+ ):
61
+ return None
62
+
63
+ # Currently we only support binary markets, hence we throw an error if we get something else.
64
+ payout_numerator_indices_gt_0 = [
65
+ idx
66
+ for idx, value in enumerate(condition_model.payoutNumerators)
67
+ if value > 0
68
+ ]
69
+ # For a binary market, there should be exactly one payout numerator greater than 0.
70
+ if len(payout_numerator_indices_gt_0) != 1:
71
+ raise ValueError(
72
+ f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators}"
73
+ )
74
+
75
+ # we return the only payout numerator greater than 0 as resolution
76
+ resolved_outcome = outcomes[payout_numerator_indices_gt_0[0]]
77
+ return Resolution.from_answer(resolved_outcome)
78
+
79
+ @staticmethod
80
+ def from_data_model(
81
+ model: PolymarketGammaResponseDataItem,
82
+ condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
83
+ ) -> "PolymarketAgentMarket":
84
+ # If len(model.markets) > 0, this denotes a categorical market.
85
+
86
+ outcomes = model.markets[0].outcomes_list
87
+ outcome_prices = model.markets[0].outcome_prices
88
+ if not outcome_prices:
89
+ # We give random prices
90
+ outcome_prices = [0.5, 0.5]
91
+ probabilities = {o: Probability(op) for o, op in zip(outcomes, outcome_prices)}
92
+
93
+ resolution = PolymarketAgentMarket.build_resolution_from_condition(
94
+ condition_id=model.markets[0].conditionId,
95
+ condition_model_dict=condition_model_dict,
96
+ outcomes=outcomes,
97
+ )
98
+
37
99
  return PolymarketAgentMarket(
38
100
  id=model.id,
39
- question=model.question,
101
+ question=model.title,
40
102
  description=model.description,
41
- outcomes=[x.outcome for x in model.tokens],
42
- resolution=model.resolution,
43
- created_time=None,
44
- close_time=model.end_date_iso,
103
+ outcomes=outcomes,
104
+ resolution=resolution,
105
+ created_time=model.startDate,
106
+ close_time=model.endDate,
45
107
  url=model.url,
46
- volume=None,
108
+ volume=CollateralToken(model.volume) if model.volume else None,
47
109
  outcome_token_pool=None,
48
- probabilities={}, # ToDo - Implement when fixing Polymarket
110
+ probabilities=probabilities,
49
111
  )
50
112
 
51
113
  def get_tiny_bet_amount(self) -> CollateralToken:
@@ -61,16 +123,11 @@ class PolymarketAgentMarket(AgentMarket):
61
123
  filter_by: FilterBy = FilterBy.OPEN,
62
124
  created_after: t.Optional[DatetimeUTC] = None,
63
125
  excluded_questions: set[str] | None = None,
64
- fetch_categorical_markets: bool = False,
65
- fetch_scalar_markets: bool = False,
126
+ market_type: MarketType = MarketType.ALL,
127
+ include_conditional_markets: bool = False,
66
128
  ) -> t.Sequence["PolymarketAgentMarket"]:
67
- if sort_by != SortBy.NONE:
68
- raise ValueError(f"Unsuported sort_by {sort_by} for Polymarket.")
69
-
70
- if created_after is not None:
71
- raise ValueError(f"Unsuported created_after for Polymarket.")
72
-
73
129
  closed: bool | None
130
+
74
131
  if filter_by == FilterBy.OPEN:
75
132
  closed = False
76
133
  elif filter_by == FilterBy.RESOLVED:
@@ -80,11 +137,37 @@ class PolymarketAgentMarket(AgentMarket):
80
137
  else:
81
138
  raise ValueError(f"Unknown filter_by: {filter_by}")
82
139
 
140
+ ascending: bool = False # default value
141
+ match sort_by:
142
+ case SortBy.NEWEST:
143
+ order_by = PolymarketOrderByEnum.START_DATE
144
+ case SortBy.CLOSING_SOONEST:
145
+ ascending = True
146
+ order_by = PolymarketOrderByEnum.END_DATE
147
+ case SortBy.HIGHEST_LIQUIDITY:
148
+ order_by = PolymarketOrderByEnum.LIQUIDITY
149
+ case SortBy.NONE:
150
+ order_by = PolymarketOrderByEnum.VOLUME_24HR
151
+ case _:
152
+ raise ValueError(f"Unknown sort_by: {sort_by}")
153
+
154
+ # closed markets also have property active=True, hence ignoring active.
155
+ markets = get_polymarkets_with_pagination(
156
+ limit=limit,
157
+ closed=closed,
158
+ order_by=order_by,
159
+ ascending=ascending,
160
+ created_after=created_after,
161
+ excluded_questions=excluded_questions,
162
+ only_binary=market_type is not MarketType.CATEGORICAL,
163
+ )
164
+
165
+ condition_models = PolymarketSubgraphHandler().get_conditions(
166
+ condition_ids=[market.markets[0].conditionId for market in markets]
167
+ )
168
+ condition_models_dict = {c.id: c for c in condition_models}
169
+
83
170
  return [
84
- PolymarketAgentMarket.from_data_model(m)
85
- for m in get_polymarket_binary_markets(
86
- limit=limit,
87
- closed=closed,
88
- excluded_questions=excluded_questions,
89
- )
171
+ PolymarketAgentMarket.from_data_model(m, condition_models_dict)
172
+ for m in markets
90
173
  ]
@@ -0,0 +1,49 @@
1
+ from pydantic import BaseModel
2
+
3
+ from prediction_market_agent_tooling.gtypes import HexBytes
4
+ from prediction_market_agent_tooling.markets.base_subgraph_handler import (
5
+ BaseSubgraphHandler,
6
+ )
7
+
8
+
9
+ class ConditionSubgraphModel(BaseModel):
10
+ id: HexBytes
11
+ payoutDenominator: int | None = None
12
+ payoutNumerators: list[int] | None = None
13
+ outcomeSlotCount: int
14
+ resolutionTimestamp: int | None = None
15
+
16
+
17
+ class PolymarketSubgraphHandler(BaseSubgraphHandler):
18
+ POLYMARKET_CONDITIONS_SUBGRAPH = "https://gateway.thegraph.com/api/{graph_api_key}/subgraphs/id/81Dm16JjuFSrqz813HysXoUPvzTwE7fsfPk2RTf66nyC"
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__()
22
+
23
+ # Load the subgraph
24
+ self.conditions_subgraph = self.sg.load_subgraph(
25
+ self.POLYMARKET_CONDITIONS_SUBGRAPH.format(
26
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
27
+ )
28
+ )
29
+
30
+ def get_conditions(
31
+ self, condition_ids: list[HexBytes]
32
+ ) -> list[ConditionSubgraphModel]:
33
+ where_stms = {"id_in": [i.hex() for i in condition_ids]}
34
+ conditions = self.conditions_subgraph.Query.conditions(
35
+ where=where_stms,
36
+ )
37
+
38
+ condition_fields = [
39
+ conditions.id,
40
+ conditions.payoutNumerators,
41
+ conditions.payoutDenominator,
42
+ conditions.outcomeSlotCount,
43
+ conditions.resolutionTimestamp,
44
+ ]
45
+
46
+ conditions_models = self.do_query(
47
+ fields=condition_fields, pydantic_model=ConditionSubgraphModel
48
+ )
49
+ return conditions_models
@@ -1,28 +1,7 @@
1
- from prediction_market_agent_tooling.markets.data_models import Resolution
2
1
  from prediction_market_agent_tooling.markets.markets import MarketType
3
- from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
4
- PolymarketFullMarket,
5
- )
6
2
  from prediction_market_agent_tooling.tools.google_utils import search_google_gcp
7
3
 
8
4
 
9
- def find_resolution_on_polymarket(question: str) -> Resolution | None:
10
- full_market = find_full_polymarket(question)
11
- # TODO: Only main markets are supported right now, add logic for others if needed.
12
- return (
13
- full_market.main_market.resolution
14
- if full_market and full_market.is_main_market
15
- else None
16
- )
17
-
18
-
19
- def find_full_polymarket(question: str) -> PolymarketFullMarket | None:
20
- polymarket_url = find_url_to_polymarket(question)
21
- return (
22
- PolymarketFullMarket.fetch_from_url(polymarket_url) if polymarket_url else None
23
- )
24
-
25
-
26
5
  def find_url_to_polymarket(question: str) -> str | None:
27
6
  # Manually create potential Polymarket's slug from the question.
28
7
  replace_chars = {
@@ -1,6 +1,5 @@
1
- import typing as t
2
-
3
1
  from cachetools import TTLCache, cached
2
+ from pydantic import BaseModel
4
3
  from web3 import Web3
5
4
 
6
5
  from prediction_market_agent_tooling.gtypes import (
@@ -18,32 +17,15 @@ from prediction_market_agent_tooling.markets.seer.exceptions import (
18
17
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
19
18
  SeerSubgraphHandler,
20
19
  )
21
- from prediction_market_agent_tooling.markets.seer.subgraph_data_models import SeerPool
22
20
  from prediction_market_agent_tooling.tools.cow.cow_order import (
23
21
  get_buy_token_amount_else_raise,
24
22
  )
25
23
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
26
24
 
27
25
 
28
- def _make_cache_key(
29
- *args: t.Any,
30
- token: ChecksumAddress,
31
- collateral_exchange_amount: CollateralToken | None = None,
32
- ) -> str:
33
- """
34
- Generate a unique cache key based on a token address and optional collateral token.
35
- """
36
-
37
- if collateral_exchange_amount is None:
38
- return f"{token}-no_collateral"
39
-
40
- return "-".join(
41
- [
42
- token,
43
- collateral_exchange_amount.symbol,
44
- str(collateral_exchange_amount.value),
45
- ]
46
- )
26
+ class Prices(BaseModel):
27
+ priceOfCollateralInAskingToken: CollateralToken
28
+ priceOfAskingTokenInCollateral: CollateralToken
47
29
 
48
30
 
49
31
  class PriceManager:
@@ -67,17 +49,17 @@ class PriceManager:
67
49
  f"{price_diff_pct=} larger than {max_price_diff=} for seer market {self.seer_market.id.hex()} "
68
50
  )
69
51
 
70
- @cached(TTLCache(maxsize=100, ttl=5 * 60), key=_make_cache_key)
71
- def get_price_for_token(
52
+ def get_price_for_token(self, token: ChecksumAddress) -> CollateralToken | None:
53
+ return self.get_amount_of_token_in_collateral(token, CollateralToken(1))
54
+
55
+ @cached(TTLCache(maxsize=100, ttl=5 * 60))
56
+ def get_amount_of_collateral_in_token(
72
57
  self,
73
58
  token: ChecksumAddress,
74
- collateral_exchange_amount: CollateralToken | None = None,
59
+ collateral_exchange_amount: CollateralToken,
75
60
  ) -> CollateralToken | None:
76
- collateral_exchange_amount = (
77
- collateral_exchange_amount
78
- if collateral_exchange_amount is not None
79
- else CollateralToken(1)
80
- )
61
+ if token == self.seer_market.collateral_token_contract_address_checksummed:
62
+ return collateral_exchange_amount
81
63
 
82
64
  try:
83
65
  buy_token_amount = get_buy_token_amount_else_raise(
@@ -85,23 +67,51 @@ class PriceManager:
85
67
  sell_token=self.seer_market.collateral_token_contract_address_checksummed,
86
68
  buy_token=token,
87
69
  )
88
- price = collateral_exchange_amount.as_wei / buy_token_amount
89
- return CollateralToken(price)
70
+ return buy_token_amount.as_token
90
71
 
91
72
  except Exception as e:
92
73
  logger.warning(
93
74
  f"Could not get quote for {token=} from Cow, exception {e=}. Falling back to pools. "
94
75
  )
95
- return self.get_token_price_from_pools(token=token)
76
+ prices = self.get_token_price_from_pools(token=token)
77
+ return (
78
+ prices.priceOfCollateralInAskingToken * collateral_exchange_amount
79
+ if prices
80
+ else None
81
+ )
96
82
 
97
- @staticmethod
98
- def pool_token0_matches_token(token: ChecksumAddress, pool: SeerPool) -> bool:
99
- return pool.token0.id.hex().lower() == token.lower()
83
+ @cached(TTLCache(maxsize=100, ttl=5 * 60))
84
+ def get_amount_of_token_in_collateral(
85
+ self,
86
+ token: ChecksumAddress,
87
+ token_exchange_amount: CollateralToken,
88
+ ) -> CollateralToken | None:
89
+ if token == self.seer_market.collateral_token_contract_address_checksummed:
90
+ return token_exchange_amount
91
+
92
+ try:
93
+ buy_collateral_amount = get_buy_token_amount_else_raise(
94
+ sell_amount=token_exchange_amount.as_wei,
95
+ sell_token=token,
96
+ buy_token=self.seer_market.collateral_token_contract_address_checksummed,
97
+ )
98
+ return buy_collateral_amount.as_token
99
+
100
+ except Exception as e:
101
+ logger.warning(
102
+ f"Could not get quote for {token=} from Cow, exception {e=}. Falling back to pools. "
103
+ )
104
+ prices = self.get_token_price_from_pools(token=token)
105
+ return (
106
+ prices.priceOfAskingTokenInCollateral * token_exchange_amount
107
+ if prices
108
+ else None
109
+ )
100
110
 
101
111
  def get_token_price_from_pools(
102
112
  self,
103
113
  token: ChecksumAddress,
104
- ) -> CollateralToken | None:
114
+ ) -> Prices | None:
105
115
  pool = SeerSubgraphHandler().get_pool_by_token(
106
116
  token_address=token,
107
117
  collateral_address=self.seer_market.collateral_token_contract_address_checksummed,
@@ -111,15 +121,24 @@ class PriceManager:
111
121
  logger.warning(f"Could not find a pool for {token=}")
112
122
  return None
113
123
 
114
- # The mapping below is odd but surprisingly the Algebra subgraph delivers the token1Price
115
- # for the token0 and the token0Price for the token1 pool.
116
- # For example, in a outcomeYES (token0)/sDAI pool (token1), token1Price is the price of outcomeYES in units of sDAI.
117
- price = (
118
- pool.token1Price
119
- if self.pool_token0_matches_token(token=token, pool=pool)
120
- else pool.token0Price
124
+ if (
125
+ Web3.to_checksum_address(pool.token0.id)
126
+ == self.seer_market.collateral_token_contract_address_checksummed
127
+ ):
128
+ price_coll_in_asking = (
129
+ pool.token1Price
130
+ ) # how many outcome tokens per 1 collateral
131
+ price_asking_in_coll = (
132
+ pool.token0Price
133
+ ) # how many collateral tokens per 1 outcome
134
+ else:
135
+ price_coll_in_asking = pool.token0Price
136
+ price_asking_in_coll = pool.token1Price
137
+
138
+ return Prices(
139
+ priceOfCollateralInAskingToken=price_coll_in_asking,
140
+ priceOfAskingTokenInCollateral=price_asking_in_coll,
121
141
  )
122
- return price
123
142
 
124
143
  def build_probability_map(self) -> dict[OutcomeStr, Probability]:
125
144
  # Inspired by https://github.com/seer-pm/demo/blob/ca682153a6b4d4dd3dcc4ad8bdcbe32202fc8fe7/web/src/hooks/useMarketOdds.ts#L15
@@ -160,6 +179,6 @@ class PriceManager:
160
179
  outcome = self.seer_market.outcomes[
161
180
  self.seer_market.wrapped_tokens.index(outcome_token)
162
181
  ]
163
- normalized_prices[OutcomeStr(outcome)] = new_price
182
+ normalized_prices[outcome] = new_price
164
183
 
165
184
  return normalized_prices