prediction-market-agent-tooling 0.68.0.dev999__py3-none-any.whl → 0.68.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 (37) 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 +13 -19
  4. prediction_market_agent_tooling/deploy/betting_strategy.py +11 -3
  5. prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
  6. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +3 -3
  7. prediction_market_agent_tooling/markets/agent_market.py +16 -9
  8. prediction_market_agent_tooling/markets/blockchain_utils.py +3 -3
  9. prediction_market_agent_tooling/markets/omen/data_models.py +3 -18
  10. prediction_market_agent_tooling/markets/omen/omen.py +26 -11
  11. prediction_market_agent_tooling/markets/omen/omen_contracts.py +2 -196
  12. prediction_market_agent_tooling/markets/omen/omen_resolving.py +2 -2
  13. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +13 -11
  14. prediction_market_agent_tooling/markets/polymarket/api.py +35 -1
  15. prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
  16. prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
  17. prediction_market_agent_tooling/markets/polymarket/data_models.py +33 -5
  18. prediction_market_agent_tooling/markets/polymarket/polymarket.py +247 -18
  19. prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
  20. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +2 -1
  21. prediction_market_agent_tooling/markets/seer/data_models.py +1 -1
  22. prediction_market_agent_tooling/markets/seer/price_manager.py +69 -1
  23. prediction_market_agent_tooling/markets/seer/seer.py +35 -20
  24. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +7 -3
  25. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +2 -0
  26. prediction_market_agent_tooling/tools/contract.py +236 -4
  27. prediction_market_agent_tooling/tools/cow/cow_order.py +13 -8
  28. prediction_market_agent_tooling/tools/hexbytes_custom.py +3 -9
  29. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +1 -1
  30. prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
  31. prediction_market_agent_tooling/tools/web3_utils.py +9 -4
  32. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/METADATA +8 -7
  33. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/RECORD +36 -34
  34. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -366
  35. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/LICENSE +0 -0
  36. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/WHEEL +0 -0
  37. {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,6 @@ from datetime import timedelta
5
5
  from enum import Enum
6
6
 
7
7
  from web3 import Web3
8
- from web3.constants import HASH_ZERO
9
8
 
10
9
  from prediction_market_agent_tooling.config import APIKeys
11
10
  from prediction_market_agent_tooling.gtypes import (
@@ -13,7 +12,6 @@ from prediction_market_agent_tooling.gtypes import (
13
12
  ChecksumAddress,
14
13
  HexAddress,
15
14
  HexBytes,
16
- HexStr,
17
15
  IPFSCIDVersion0,
18
16
  OutcomeWei,
19
17
  TxParams,
@@ -24,11 +22,9 @@ from prediction_market_agent_tooling.gtypes import (
24
22
  )
25
23
  from prediction_market_agent_tooling.markets.omen.data_models import (
26
24
  INVALID_ANSWER_HEX_BYTES,
27
- ConditionPreparationEvent,
28
25
  ContractPrediction,
29
26
  FPMMFundingAddedEvent,
30
27
  OmenFixedProductMarketMakerCreationEvent,
31
- PayoutRedemptionEvent,
32
28
  RealitioLogNewQuestionEvent,
33
29
  format_realitio_question,
34
30
  )
@@ -38,6 +34,7 @@ from prediction_market_agent_tooling.markets.omen.omen_constants import (
38
34
  WRAPPED_XDAI_CONTRACT_ADDRESS,
39
35
  )
40
36
  from prediction_market_agent_tooling.tools.contract import (
37
+ ConditionalTokenContract,
41
38
  ContractDepositableWrapperERC20OnGnosisChain,
42
39
  ContractERC20OnGnosisChain,
43
40
  ContractERC4626OnGnosisChain,
@@ -96,202 +93,11 @@ class OmenOracleContract(ContractOnGnosisChain):
96
93
  )
97
94
 
98
95
 
99
- def build_parent_collection_id() -> HexStr:
100
- return HASH_ZERO # Taken from Olas
101
-
102
-
103
- class OmenConditionalTokenContract(ContractOnGnosisChain):
104
- # Contract ABI taken from https://gnosisscan.io/address/0xCeAfDD6bc0bEF976fdCd1112955828E00543c0Ce#code.
105
- abi: ABI = abi_field_validator(
106
- os.path.join(
107
- os.path.dirname(os.path.realpath(__file__)),
108
- "../../abis/omen_fpmm_conditionaltokens.abi.json",
109
- )
110
- )
96
+ class OmenConditionalTokenContract(ConditionalTokenContract, ContractOnGnosisChain):
111
97
  address: ChecksumAddress = Web3.to_checksum_address(
112
98
  "0xCeAfDD6bc0bEF976fdCd1112955828E00543c0Ce"
113
99
  )
114
100
 
115
- def getConditionId(
116
- self,
117
- question_id: HexBytes,
118
- oracle_address: ChecksumAddress,
119
- outcomes_slot_count: int,
120
- web3: Web3 | None = None,
121
- ) -> HexBytes:
122
- id_ = HexBytes(
123
- self.call(
124
- "getConditionId",
125
- [oracle_address, question_id, outcomes_slot_count],
126
- web3=web3,
127
- )
128
- )
129
- return id_
130
-
131
- def balanceOf(
132
- self, from_address: ChecksumAddress, position_id: int, web3: Web3 | None = None
133
- ) -> OutcomeWei:
134
- balance = OutcomeWei(
135
- self.call("balanceOf", [from_address, position_id], web3=web3)
136
- )
137
- return balance
138
-
139
- def getCollectionId(
140
- self,
141
- parent_collection_id: HexStr,
142
- condition_id: HexBytes,
143
- index_set: int,
144
- web3: Web3 | None = None,
145
- ) -> HexBytes:
146
- collection_id = HexBytes(
147
- self.call(
148
- "getCollectionId",
149
- [parent_collection_id, condition_id, index_set],
150
- web3=web3,
151
- )
152
- )
153
- return collection_id
154
-
155
- def getPositionId(
156
- self,
157
- collateral_token_address: ChecksumAddress,
158
- collection_id: HexBytes,
159
- web3: Web3 | None = None,
160
- ) -> int:
161
- position_id: int = self.call(
162
- "getPositionId",
163
- [collateral_token_address, collection_id],
164
- web3=web3,
165
- )
166
- return position_id
167
-
168
- def mergePositions(
169
- self,
170
- api_keys: APIKeys,
171
- collateral_token_address: ChecksumAddress,
172
- conditionId: HexBytes,
173
- index_sets: t.List[int],
174
- amount: OutcomeWei,
175
- parent_collection_id: HexStr = build_parent_collection_id(),
176
- web3: Web3 | None = None,
177
- ) -> TxReceipt:
178
- return self.send(
179
- api_keys=api_keys,
180
- function_name="mergePositions",
181
- function_params=[
182
- collateral_token_address,
183
- parent_collection_id,
184
- conditionId,
185
- index_sets,
186
- amount,
187
- ],
188
- web3=web3,
189
- )
190
-
191
- def redeemPositions(
192
- self,
193
- api_keys: APIKeys,
194
- collateral_token_address: HexAddress,
195
- condition_id: HexBytes,
196
- index_sets: t.List[int],
197
- parent_collection_id: HexStr = build_parent_collection_id(),
198
- web3: Web3 | None = None,
199
- ) -> PayoutRedemptionEvent:
200
- receipt_tx = self.send(
201
- api_keys=api_keys,
202
- function_name="redeemPositions",
203
- function_params=[
204
- collateral_token_address,
205
- parent_collection_id,
206
- condition_id,
207
- index_sets,
208
- ],
209
- web3=web3,
210
- )
211
- redeem_event_logs = (
212
- self.get_web3_contract(web3=web3)
213
- .events.PayoutRedemption()
214
- .process_receipt(receipt_tx)
215
- )
216
- redeem_event = PayoutRedemptionEvent(**redeem_event_logs[0]["args"])
217
- return redeem_event
218
-
219
- def getOutcomeSlotCount(
220
- self, condition_id: HexBytes, web3: Web3 | None = None
221
- ) -> int:
222
- count: int = self.call("getOutcomeSlotCount", [condition_id], web3=web3)
223
- return count
224
-
225
- def does_condition_exists(
226
- self, condition_id: HexBytes, web3: Web3 | None = None
227
- ) -> bool:
228
- return self.getOutcomeSlotCount(condition_id, web3=web3) > 0
229
-
230
- def is_condition_resolved(
231
- self, condition_id: HexBytes, web3: Web3 | None = None
232
- ) -> bool:
233
- # from ConditionalTokens.redeemPositions:
234
- # uint den = payoutDenominator[conditionId]; require(den > 0, "result for condition not received yet");
235
- payout_for_condition = self.payoutDenominator(condition_id, web3=web3)
236
- return payout_for_condition > 0
237
-
238
- def payoutDenominator(
239
- self, condition_id: HexBytes, web3: Web3 | None = None
240
- ) -> int:
241
- payoutForCondition: int = self.call(
242
- "payoutDenominator", [condition_id], web3=web3
243
- )
244
- return payoutForCondition
245
-
246
- def setApprovalForAll(
247
- self,
248
- api_keys: APIKeys,
249
- for_address: ChecksumAddress,
250
- approve: bool,
251
- tx_params: t.Optional[TxParams] = None,
252
- web3: Web3 | None = None,
253
- ) -> TxReceipt:
254
- return self.send(
255
- api_keys=api_keys,
256
- function_name="setApprovalForAll",
257
- function_params=[
258
- for_address,
259
- approve,
260
- ],
261
- tx_params=tx_params,
262
- web3=web3,
263
- )
264
-
265
- def prepareCondition(
266
- self,
267
- api_keys: APIKeys,
268
- oracle_address: ChecksumAddress,
269
- question_id: HexBytes,
270
- outcomes_slot_count: int,
271
- tx_params: t.Optional[TxParams] = None,
272
- web3: Web3 | None = None,
273
- ) -> ConditionPreparationEvent:
274
- receipt_tx = self.send(
275
- api_keys=api_keys,
276
- function_name="prepareCondition",
277
- function_params=[
278
- oracle_address,
279
- question_id,
280
- outcomes_slot_count,
281
- ],
282
- tx_params=tx_params,
283
- web3=web3,
284
- )
285
-
286
- event_logs = (
287
- self.get_web3_contract(web3=web3)
288
- .events.ConditionPreparation()
289
- .process_receipt(receipt_tx)
290
- )
291
- cond_event = ConditionPreparationEvent(**event_logs[0]["args"])
292
-
293
- return cond_event
294
-
295
101
 
296
102
  class OmenFixedProductMarketMakerContract(ContractOnGnosisChain):
297
103
  # File content taken from https://github.com/protofire/omen-exchange/blob/master/app/src/abi/marketMaker.json.
@@ -85,10 +85,10 @@ def claim_bonds_on_realitio_question(
85
85
  responses = sorted(responses, key=lambda x: x.timestamp)
86
86
 
87
87
  if not responses:
88
- raise ValueError(f"No answers found for {question.questionId.hex()=}")
88
+ raise ValueError(f"No answers found for {question.questionId.to_0x_hex()=}")
89
89
 
90
90
  if responses[-1].question.historyHash == ZERO_BYTES:
91
- raise ValueError(f"Already claimed {question.questionId.hex()=}.")
91
+ raise ValueError(f"Already claimed {question.questionId.to_0x_hex()=}.")
92
92
 
93
93
  history_hashes: list[HexBytes] = []
94
94
  addresses: list[ChecksumAddress] = []
@@ -285,7 +285,7 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
285
285
  where_stms["liquidityParameter_gt"] = liquidity_bigger_than
286
286
 
287
287
  if condition_id_in is not None:
288
- where_stms["condition_"]["id_in"] = [x.hex() for x in condition_id_in]
288
+ where_stms["condition_"]["id_in"] = [x.to_0x_hex() for x in condition_id_in]
289
289
 
290
290
  if id_in is not None:
291
291
  where_stms["id_in"] = [i.lower() for i in id_in]
@@ -513,7 +513,7 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
513
513
  where_stms: dict[str, t.Any] = {}
514
514
 
515
515
  if condition_id is not None:
516
- where_stms["conditionIds_contains"] = [condition_id.hex()]
516
+ where_stms["conditionIds_contains"] = [condition_id.to_0x_hex()]
517
517
 
518
518
  positions = self.conditional_tokens_subgraph.Query.positions(
519
519
  first=sys.maxsize, where=unwrap_generic_value(where_stms)
@@ -541,10 +541,12 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
541
541
  where_stms["totalBalance_gt"] = total_balance_bigger_than
542
542
 
543
543
  if user_position_id_in is not None:
544
- where_stms["id_in"] = [x.hex() for x in user_position_id_in]
544
+ where_stms["id_in"] = [x.to_0x_hex() for x in user_position_id_in]
545
545
 
546
546
  if position_id_in is not None:
547
- where_stms["position_"]["positionId_in"] = [x.hex() for x in position_id_in]
547
+ where_stms["position_"]["positionId_in"] = [
548
+ x.to_0x_hex() for x in position_id_in
549
+ ]
548
550
 
549
551
  positions = self.conditional_tokens_subgraph.Query.userPositions(
550
552
  first=sys.maxsize, where=unwrap_generic_value(where_stms)
@@ -710,13 +712,13 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
710
712
  where_stms["user"] = user.lower()
711
713
 
712
714
  if question_id is not None:
713
- where_stms["questionId"] = question_id.hex()
715
+ where_stms["questionId"] = question_id.to_0x_hex()
714
716
 
715
717
  if claimed is not None:
716
718
  if claimed:
717
- where_stms["historyHash"] = ZERO_BYTES.hex()
719
+ where_stms["historyHash"] = ZERO_BYTES.to_0x_hex()
718
720
  else:
719
- where_stms["historyHash_not"] = ZERO_BYTES.hex()
721
+ where_stms["historyHash_not"] = ZERO_BYTES.to_0x_hex()
720
722
 
721
723
  if current_answer_before is not None:
722
724
  where_stms["currentAnswerTimestamp_lt"] = to_int_timestamp(
@@ -750,7 +752,7 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
750
752
 
751
753
  if question_id_in is not None:
752
754
  # Be aware: On Omen subgraph, question's `id` represents `questionId` on reality subgraph. And `id` on reality subraph is just a weird concat of multiple things from the question.
753
- where_stms["questionId_in"] = [x.hex() for x in question_id_in]
755
+ where_stms["questionId_in"] = [x.to_0x_hex() for x in question_id_in]
754
756
 
755
757
  if excluded_titles:
756
758
  # Be aware: This is called `title_not_in` on Omen subgraph.
@@ -777,7 +779,7 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
777
779
  where_stms: dict[str, t.Any] = {}
778
780
 
779
781
  if question_id is not None:
780
- where_stms["id"] = question_id.hex()
782
+ where_stms["id"] = question_id.to_0x_hex()
781
783
 
782
784
  if current_answer_before is not None:
783
785
  where_stms["currentAnswerTimestamp_lt"] = to_int_timestamp(
@@ -811,7 +813,7 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
811
813
 
812
814
  if question_id_in is not None:
813
815
  # Be aware: On Omen subgraph, question's `id` represents `questionId` on reality subgraph. And `id` on reality subraph is just a weird concat of multiple things from the question.
814
- where_stms["id_in"] = [x.hex() for x in question_id_in]
816
+ where_stms["id_in"] = [x.to_0x_hex() for x in question_id_in]
815
817
 
816
818
  if excluded_titles:
817
819
  # Be aware: This is called `qTitle_not_in` on Omen subgraph.
@@ -864,7 +866,7 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
864
866
  answer = self.realityeth_subgraph.Answer
865
867
  # subgrounds complains if bytes is passed, hence we convert it to HexStr
866
868
  where_stms = [
867
- answer.question.questionId == question_id.hex(),
869
+ answer.question.questionId == question_id.to_0x_hex(),
868
870
  ]
869
871
 
870
872
  answers = self.realityeth_subgraph.Query.answers(
@@ -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}"