prediction-market-agent-tooling 0.68.0.dev999__py3-none-any.whl → 0.69.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.
Files changed (42) hide show
  1. prediction_market_agent_tooling/chains.py +1 -0
  2. prediction_market_agent_tooling/config.py +37 -2
  3. prediction_market_agent_tooling/deploy/agent.py +26 -21
  4. prediction_market_agent_tooling/deploy/betting_strategy.py +133 -22
  5. prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
  6. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +17 -20
  7. prediction_market_agent_tooling/markets/agent_market.py +27 -9
  8. prediction_market_agent_tooling/markets/blockchain_utils.py +3 -3
  9. prediction_market_agent_tooling/markets/markets.py +16 -0
  10. prediction_market_agent_tooling/markets/omen/data_models.py +3 -18
  11. prediction_market_agent_tooling/markets/omen/omen.py +26 -11
  12. prediction_market_agent_tooling/markets/omen/omen_contracts.py +2 -196
  13. prediction_market_agent_tooling/markets/omen/omen_resolving.py +2 -2
  14. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +13 -11
  15. prediction_market_agent_tooling/markets/polymarket/api.py +35 -1
  16. prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
  17. prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
  18. prediction_market_agent_tooling/markets/polymarket/data_models.py +33 -5
  19. prediction_market_agent_tooling/markets/polymarket/polymarket.py +247 -18
  20. prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
  21. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +2 -1
  22. prediction_market_agent_tooling/markets/seer/data_models.py +41 -6
  23. prediction_market_agent_tooling/markets/seer/price_manager.py +69 -1
  24. prediction_market_agent_tooling/markets/seer/seer.py +77 -26
  25. prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
  26. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +71 -20
  27. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +67 -0
  28. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +17 -22
  29. prediction_market_agent_tooling/tools/contract.py +236 -4
  30. prediction_market_agent_tooling/tools/cow/cow_order.py +13 -8
  31. prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
  32. prediction_market_agent_tooling/tools/hexbytes_custom.py +3 -9
  33. prediction_market_agent_tooling/tools/langfuse_client_utils.py +17 -5
  34. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +2 -2
  35. prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
  36. prediction_market_agent_tooling/tools/web3_utils.py +9 -4
  37. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/METADATA +8 -7
  38. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/RECORD +41 -38
  39. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -366
  40. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/LICENSE +0 -0
  41. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/WHEEL +0 -0
  42. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/entry_points.txt +0 -0
@@ -6,12 +6,14 @@ from urllib.parse import urljoin
6
6
  import httpx
7
7
  import tenacity
8
8
 
9
+ from prediction_market_agent_tooling.gtypes import ChecksumAddress, HexBytes
9
10
  from prediction_market_agent_tooling.loggers import logger
10
11
  from prediction_market_agent_tooling.markets.polymarket.data_models import (
11
12
  POLYMARKET_FALSE_OUTCOME,
12
13
  POLYMARKET_TRUE_OUTCOME,
13
14
  PolymarketGammaResponse,
14
15
  PolymarketGammaResponseDataItem,
16
+ PolymarketPositionResponse,
15
17
  )
16
18
  from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
17
19
  from prediction_market_agent_tooling.tools.httpx_cached_client import HttpxCachedClient
@@ -84,7 +86,7 @@ def get_polymarkets_with_pagination(
84
86
  markets_to_add = []
85
87
  for m in market_response.data:
86
88
  # Some Polymarket markets are missing the markets field
87
- if m.markets is None:
89
+ if m.markets is None or m.markets[0].clobTokenIds is None:
88
90
  continue
89
91
  if excluded_questions and m.title in excluded_questions:
90
92
  continue
@@ -127,3 +129,35 @@ def get_polymarkets_with_pagination(
127
129
 
128
130
  # Return exactly the number of items requested (in case we got more due to batch size)
129
131
  return all_markets[:limit]
132
+
133
+
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}
158
+
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)
@@ -4,14 +4,14 @@ from pydantic import BaseModel
4
4
 
5
5
  from prediction_market_agent_tooling.gtypes import USDC, OutcomeStr, Probability
6
6
  from prediction_market_agent_tooling.markets.data_models import Resolution
7
- from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
8
- POLYMARKET_FALSE_OUTCOME,
9
- POLYMARKET_TRUE_OUTCOME,
10
- construct_polymarket_url,
11
- )
12
7
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
13
8
  from prediction_market_agent_tooling.tools.utils import DatetimeUTC
14
9
 
10
+ POLYMARKET_TRUE_OUTCOME = "Yes"
11
+ POLYMARKET_FALSE_OUTCOME = "No"
12
+
13
+ POLYMARKET_BASE_URL = "https://polymarket.com"
14
+
15
15
 
16
16
  class PolymarketRewards(BaseModel):
17
17
  min_size: int
@@ -39,6 +39,13 @@ class PolymarketGammaMarket(BaseModel):
39
39
  questionId: str | None = None
40
40
  clobTokenIds: str | None = None # int-encoded hex
41
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
+
42
49
  @property
43
50
  def outcomes_list(self) -> list[OutcomeStr]:
44
51
  return [OutcomeStr(i) for i in json.loads(self.outcomes)]
@@ -186,3 +193,24 @@ class PolymarketMarketWithPrices(PolymarketMarket):
186
193
  raise ValueError(
187
194
  "Should not happen, as we filter only for binary markets in get_polymarket_binary_markets."
188
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}"
@@ -1,11 +1,19 @@
1
1
  import typing as t
2
2
 
3
+ import cachetools
4
+ from web3 import Web3
5
+
6
+ from prediction_market_agent_tooling.config import APIKeys, RPCConfig
3
7
  from prediction_market_agent_tooling.gtypes import (
4
8
  USD,
9
+ ChecksumAddress,
5
10
  CollateralToken,
6
11
  HexBytes,
7
12
  OutcomeStr,
13
+ OutcomeToken,
8
14
  Probability,
15
+ Wei,
16
+ xDai,
9
17
  )
10
18
  from prediction_market_agent_tooling.loggers import logger
11
19
  from prediction_market_agent_tooling.markets.agent_market import (
@@ -13,27 +21,45 @@ from prediction_market_agent_tooling.markets.agent_market import (
13
21
  ConditionalFilterType,
14
22
  FilterBy,
15
23
  MarketFees,
24
+ ProcessedMarket,
16
25
  QuestionType,
17
26
  SortBy,
18
27
  )
19
- from prediction_market_agent_tooling.markets.data_models import Resolution
28
+ from prediction_market_agent_tooling.markets.data_models import (
29
+ ExistingPosition,
30
+ Resolution,
31
+ )
20
32
  from prediction_market_agent_tooling.markets.polymarket.api import (
21
33
  PolymarketOrderByEnum,
22
34
  get_polymarkets_with_pagination,
35
+ get_user_positions,
36
+ )
37
+ from prediction_market_agent_tooling.markets.polymarket.clob_manager import (
38
+ ClobManager,
39
+ PolymarketPriceSideEnum,
40
+ )
41
+ from prediction_market_agent_tooling.markets.polymarket.constants import (
42
+ POLYMARKET_TINY_BET_AMOUNT,
23
43
  )
24
44
  from prediction_market_agent_tooling.markets.polymarket.data_models import (
45
+ POLYMARKET_BASE_URL,
25
46
  PolymarketGammaResponseDataItem,
26
47
  )
27
- from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
28
- POLYMARKET_BASE_URL,
48
+ from prediction_market_agent_tooling.markets.polymarket.polymarket_contracts import (
49
+ USDCeContract,
29
50
  )
30
51
  from prediction_market_agent_tooling.markets.polymarket.polymarket_subgraph_handler import (
31
52
  ConditionSubgraphModel,
32
53
  PolymarketSubgraphHandler,
33
54
  )
34
55
  from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
56
+ from prediction_market_agent_tooling.tools.tokens.usd import get_token_in_usd
35
57
  from prediction_market_agent_tooling.tools.utils import check_not_none
36
58
 
59
+ SHARED_CACHE: cachetools.TTLCache[t.Hashable, t.Any] = cachetools.TTLCache(
60
+ maxsize=256, ttl=10 * 60
61
+ )
62
+
37
63
 
38
64
  class PolymarketAgentMarket(AgentMarket):
39
65
  """
@@ -47,6 +73,15 @@ class PolymarketAgentMarket(AgentMarket):
47
73
  # But then in the new subgraph API, they have `fee: BigInt! (Percentage fee of trades taken by market maker. A 2% fee is represented as 2*10^16)`.
48
74
  # TODO: Check out the fees while integrating the subgraph API or if we implement placing of bets on Polymarket.
49
75
  fees: MarketFees = MarketFees.get_zero_fees()
76
+ condition_id: HexBytes
77
+ liquidity_usd: USD
78
+ token_ids: list[int]
79
+ closed_flag_from_polymarket: bool
80
+ active_flag_from_polymarket: bool
81
+
82
+ @staticmethod
83
+ def collateral_token_address() -> ChecksumAddress:
84
+ return USDCeContract().address
50
85
 
51
86
  @staticmethod
52
87
  def build_resolution_from_condition(
@@ -73,7 +108,7 @@ class PolymarketAgentMarket(AgentMarket):
73
108
  if len(payout_numerator_indices_gt_0) != 1:
74
109
  # These cases involve multi-categorical resolution (to be implemented https://github.com/gnosis/prediction-market-agent-tooling/issues/770)
75
110
  logger.warning(
76
- f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators} for condition_id {condition_id.hex()}"
111
+ f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators} for condition_id {condition_id.to_0x_hex()}"
77
112
  )
78
113
  return Resolution(outcome=None, invalid=False)
79
114
 
@@ -81,42 +116,68 @@ class PolymarketAgentMarket(AgentMarket):
81
116
  resolved_outcome = outcomes[payout_numerator_indices_gt_0[0]]
82
117
  return Resolution.from_answer(resolved_outcome)
83
118
 
119
+ def get_token_id_for_outcome(self, outcome: OutcomeStr) -> int:
120
+ outcome_idx = self.outcomes.index(outcome)
121
+ return self.token_ids[outcome_idx]
122
+
84
123
  @staticmethod
85
124
  def from_data_model(
86
125
  model: PolymarketGammaResponseDataItem,
87
126
  condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
88
- ) -> "PolymarketAgentMarket":
127
+ ) -> t.Optional["PolymarketAgentMarket"]:
89
128
  # If len(model.markets) > 0, this denotes a categorical market.
90
129
  markets = check_not_none(model.markets)
91
130
  outcomes = markets[0].outcomes_list
92
131
  outcome_prices = markets[0].outcome_prices
93
132
  if not outcome_prices:
94
- # We give random prices
95
- outcome_prices = [0.5, 0.5]
133
+ logger.info(f"Market has no outcome prices. Skipping. {model=}")
134
+ return None
135
+
96
136
  probabilities = {o: Probability(op) for o, op in zip(outcomes, outcome_prices)}
97
137
 
138
+ condition_id = markets[0].conditionId
98
139
  resolution = PolymarketAgentMarket.build_resolution_from_condition(
99
- condition_id=markets[0].conditionId,
140
+ condition_id=condition_id,
100
141
  condition_model_dict=condition_model_dict,
101
142
  outcomes=outcomes,
102
143
  )
103
144
 
104
145
  return PolymarketAgentMarket(
105
146
  id=model.id,
147
+ condition_id=condition_id,
106
148
  question=model.title,
107
149
  description=model.description,
108
150
  outcomes=outcomes,
109
151
  resolution=resolution,
110
152
  created_time=model.startDate,
111
153
  close_time=model.endDate,
154
+ closed_flag_from_polymarket=model.closed,
155
+ active_flag_from_polymarket=model.active,
112
156
  url=model.url,
113
157
  volume=CollateralToken(model.volume) if model.volume else None,
114
158
  outcome_token_pool=None,
115
159
  probabilities=probabilities,
160
+ liquidity_usd=USD(model.liquidity)
161
+ if model.liquidity is not None
162
+ else USD(0),
163
+ token_ids=markets[0].token_ids,
116
164
  )
117
165
 
118
166
  def get_tiny_bet_amount(self) -> CollateralToken:
119
- raise NotImplementedError("TODO: Implement to allow betting on Polymarket.")
167
+ return CollateralToken(POLYMARKET_TINY_BET_AMOUNT.value)
168
+
169
+ def get_token_in_usd(self, x: CollateralToken) -> USD:
170
+ return get_token_in_usd(x, self.collateral_token_address())
171
+
172
+ @staticmethod
173
+ def get_trade_balance(api_keys: APIKeys, web3: Web3 | None = None) -> USD:
174
+ usdc_balance_wei = USDCeContract().balanceOf(
175
+ for_address=api_keys.public_key, web3=web3
176
+ )
177
+ return USD(usdc_balance_wei.value * 1e-6)
178
+
179
+ def get_liquidity(self, web3: Web3 | None = None) -> CollateralToken:
180
+ return CollateralToken(self.liquidity_usd.value)
120
181
 
121
182
  def place_bet(self, outcome: OutcomeStr, amount: USD) -> str:
122
183
  raise NotImplementedError("TODO: Implement to allow betting on Polymarket.")
@@ -146,6 +207,7 @@ class PolymarketAgentMarket(AgentMarket):
146
207
  match sort_by:
147
208
  case SortBy.NEWEST:
148
209
  order_by = PolymarketOrderByEnum.START_DATE
210
+ ascending = False
149
211
  case SortBy.CLOSING_SOONEST:
150
212
  ascending = True
151
213
  order_by = PolymarketOrderByEnum.END_DATE
@@ -168,15 +230,182 @@ class PolymarketAgentMarket(AgentMarket):
168
230
  )
169
231
 
170
232
  condition_models = PolymarketSubgraphHandler().get_conditions(
171
- condition_ids=[
172
- market.markets[0].conditionId
173
- for market in markets
174
- if market.markets is not None
175
- ]
233
+ condition_ids=list(
234
+ set(
235
+ [
236
+ market.markets[0].conditionId
237
+ for market in markets
238
+ if market.markets is not None
239
+ ]
240
+ )
241
+ )
176
242
  )
177
243
  condition_models_dict = {c.id: c for c in condition_models}
178
244
 
179
- return [
180
- PolymarketAgentMarket.from_data_model(m, condition_models_dict)
181
- for m in markets
182
- ]
245
+ result_markets: list[PolymarketAgentMarket] = []
246
+ for m in markets:
247
+ market = PolymarketAgentMarket.from_data_model(m, condition_models_dict)
248
+ if market is not None:
249
+ result_markets.append(market)
250
+ return result_markets
251
+
252
+ def ensure_min_native_balance(
253
+ self,
254
+ min_required_balance: xDai,
255
+ multiplier: float = 3.0,
256
+ web3: Web3 | None = None,
257
+ ) -> None:
258
+ balance_collateral = USDCeContract().balanceOf(
259
+ for_address=APIKeys().public_key, web3=web3
260
+ )
261
+ # USDC has 6 decimals, xDAI has 18. We convert from Wei into atomic units.
262
+ balance_collateral_atomic = CollateralToken(float(balance_collateral) / 1e6)
263
+ if balance_collateral_atomic < min_required_balance.as_token:
264
+ raise EnvironmentError(
265
+ f"USDC balance {balance_collateral_atomic} < {min_required_balance.as_token=}"
266
+ )
267
+
268
+ @staticmethod
269
+ def redeem_winnings(api_keys: APIKeys) -> None:
270
+ # ToDo - implement me - https://github.com/gnosis/prediction-market-agent-tooling/issues/824
271
+ pass
272
+
273
+ @staticmethod
274
+ def verify_operational_balance(api_keys: APIKeys) -> bool:
275
+ """Method for checking if agent has enough funds to pay for gas fees."""
276
+ web3 = RPCConfig().get_polygon_web3()
277
+ pol_balance: Wei = Wei(web3.eth.get_balance(api_keys.public_key))
278
+ return pol_balance > Wei(int(0.001 * 1e18))
279
+
280
+ def store_prediction(
281
+ self,
282
+ processed_market: ProcessedMarket | None,
283
+ keys: APIKeys,
284
+ agent_name: str,
285
+ ) -> None:
286
+ pass
287
+
288
+ def store_trades(
289
+ self,
290
+ traded_market: ProcessedMarket | None,
291
+ keys: APIKeys,
292
+ agent_name: str,
293
+ web3: Web3 | None = None,
294
+ ) -> None:
295
+ logger.info("Storing trades deactivated for Polymarket.")
296
+ # Understand how market_id can be represented.
297
+ # Condition_id could work but length doesn't seem to match.
298
+
299
+ @classmethod
300
+ def get_user_url(cls, keys: APIKeys) -> str:
301
+ return f"https://polymarket.com/{keys.public_key}"
302
+
303
+ def get_position(
304
+ self, user_id: str, web3: Web3 | None = None
305
+ ) -> ExistingPosition | None:
306
+ """
307
+ Fetches position from the user in a given market.
308
+ """
309
+ positions = get_user_positions(
310
+ user_id=Web3.to_checksum_address(user_id), condition_ids=[self.condition_id]
311
+ )
312
+ if not positions:
313
+ return None
314
+
315
+ amounts_ot = {i: OutcomeToken(0) for i in self.outcomes}
316
+ amounts_potential = {i: USD(0) for i in self.outcomes}
317
+ amounts_current = {i: USD(0) for i in self.outcomes}
318
+
319
+ for p in positions:
320
+ if p.conditionId != self.condition_id.to_0x_hex():
321
+ continue
322
+
323
+ amounts_potential[OutcomeStr(p.outcome)] = USD(p.size)
324
+ amounts_ot[OutcomeStr(p.outcome)] = OutcomeToken(p.size)
325
+ amounts_current[OutcomeStr(p.outcome)] = USD(p.currentValue)
326
+
327
+ return ExistingPosition(
328
+ amounts_potential=amounts_potential,
329
+ amounts_ot=amounts_ot,
330
+ market_id=self.id,
331
+ amounts_current=amounts_current,
332
+ )
333
+
334
+ def can_be_traded(self) -> bool:
335
+ return (
336
+ self.active_flag_from_polymarket
337
+ and not self.closed_flag_from_polymarket
338
+ and self.liquidity_usd
339
+ > USD(5) # we conservatively require some positive liquidity to trade on
340
+ )
341
+
342
+ def get_buy_token_amount(
343
+ self, bet_amount: USD | CollateralToken, outcome_str: OutcomeStr
344
+ ) -> OutcomeToken:
345
+ """Returns number of outcome tokens returned for a given bet expressed in collateral units."""
346
+
347
+ if outcome_str not in self.outcomes:
348
+ raise ValueError(
349
+ f"Outcome {outcome_str} not found in market outcomes {self.outcomes}"
350
+ )
351
+
352
+ token_id = self.get_token_id_for_outcome(outcome_str)
353
+
354
+ price = ClobManager(APIKeys()).get_token_price(
355
+ token_id=token_id, side=PolymarketPriceSideEnum.BUY
356
+ )
357
+ if not price:
358
+ raise ValueError(
359
+ f"Could not get price for outcome {outcome_str} with token_id {token_id}"
360
+ )
361
+
362
+ # we work with floats since USD and Collateral are the same on Polymarket
363
+ buy_token_amount = bet_amount.value / price.value
364
+ logger.info(f"Buy token amount: {buy_token_amount=}")
365
+ return OutcomeToken(buy_token_amount)
366
+
367
+ def buy_tokens(self, outcome: OutcomeStr, amount: USD) -> str:
368
+ clob_manager = ClobManager(APIKeys())
369
+ token_id = self.get_token_id_for_outcome(outcome)
370
+
371
+ created_order = clob_manager.place_buy_market_order(
372
+ token_id=token_id, usdc_amount=amount
373
+ )
374
+ if not created_order.success:
375
+ raise ValueError(f"Error creating order: {created_order}")
376
+
377
+ return created_order.transactionsHashes[0].to_0x_hex()
378
+
379
+ def sell_tokens(
380
+ self,
381
+ outcome: OutcomeStr,
382
+ amount: USD | OutcomeToken,
383
+ api_keys: APIKeys | None = None,
384
+ ) -> str:
385
+ """
386
+ Polymarket's API expect shares to be sold. 1 share == 1 outcome token / 1e6.
387
+ The number of outcome tokens matches the `balanceOf` of the conditionalTokens contract.
388
+ In comparison, the number of shares match the position.size from the user position.
389
+ """
390
+ logger.info(f"Selling {amount=} from {outcome=}")
391
+ clob_manager = ClobManager(api_keys=api_keys or APIKeys())
392
+ token_id = self.get_token_id_for_outcome(outcome)
393
+ token_shares: OutcomeToken
394
+ if isinstance(amount, OutcomeToken):
395
+ token_shares = amount
396
+ elif isinstance(amount, USD):
397
+ token_price = clob_manager.get_token_price(
398
+ token_id=token_id, side=PolymarketPriceSideEnum.SELL
399
+ )
400
+ # We expect that our order sizes don't move the price too much.
401
+ token_shares = OutcomeToken(amount.value / token_price.value)
402
+ else:
403
+ raise ValueError(f"Unsupported amount type {type(amount)}")
404
+
405
+ created_order = clob_manager.place_sell_market_order(
406
+ token_id=token_id, token_shares=token_shares
407
+ )
408
+ if not created_order.success:
409
+ raise ValueError(f"Error creating order: {created_order}")
410
+
411
+ return created_order.transactionsHashes[0].to_0x_hex()
@@ -0,0 +1,35 @@
1
+ from web3 import Web3
2
+
3
+ from prediction_market_agent_tooling.config import APIKeys
4
+ from prediction_market_agent_tooling.gtypes import ChecksumAddress
5
+ from prediction_market_agent_tooling.tools.contract import (
6
+ ConditionalTokenContract,
7
+ ContractERC20BaseClass,
8
+ ContractOnPolygonChain,
9
+ )
10
+
11
+
12
+ class USDCeContract(ContractERC20BaseClass, ContractOnPolygonChain):
13
+ # USDC.e is used by Polymarket.
14
+ address: ChecksumAddress = Web3.to_checksum_address(
15
+ "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
16
+ )
17
+
18
+
19
+ class PolymarketConditionalTokenContract(
20
+ ConditionalTokenContract, ContractOnPolygonChain
21
+ ):
22
+ address: ChecksumAddress = Web3.to_checksum_address(
23
+ "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
24
+ )
25
+
26
+ def approve_if_not_approved(
27
+ self, api_keys: APIKeys, for_address: ChecksumAddress, web3: Web3 | None = None
28
+ ) -> None:
29
+ is_approved = self.isApprovedForAll(
30
+ owner=api_keys.public_key, for_address=for_address, web3=web3
31
+ )
32
+ if not is_approved:
33
+ self.setApprovalForAll(
34
+ api_keys=api_keys, for_address=for_address, approve=True, web3=web3
35
+ )