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.
- prediction_market_agent_tooling/chains.py +1 -0
- prediction_market_agent_tooling/config.py +37 -2
- prediction_market_agent_tooling/deploy/agent.py +13 -19
- prediction_market_agent_tooling/deploy/betting_strategy.py +11 -3
- prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +3 -3
- prediction_market_agent_tooling/markets/agent_market.py +16 -9
- prediction_market_agent_tooling/markets/blockchain_utils.py +3 -3
- prediction_market_agent_tooling/markets/omen/data_models.py +3 -18
- prediction_market_agent_tooling/markets/omen/omen.py +26 -11
- prediction_market_agent_tooling/markets/omen/omen_contracts.py +2 -196
- prediction_market_agent_tooling/markets/omen/omen_resolving.py +2 -2
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +13 -11
- prediction_market_agent_tooling/markets/polymarket/api.py +35 -1
- prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
- prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
- prediction_market_agent_tooling/markets/polymarket/data_models.py +33 -5
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +247 -18
- prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
- prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +2 -1
- prediction_market_agent_tooling/markets/seer/data_models.py +1 -1
- prediction_market_agent_tooling/markets/seer/price_manager.py +69 -1
- prediction_market_agent_tooling/markets/seer/seer.py +35 -20
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +7 -3
- prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +2 -0
- prediction_market_agent_tooling/tools/contract.py +236 -4
- prediction_market_agent_tooling/tools/cow/cow_order.py +13 -8
- prediction_market_agent_tooling/tools/hexbytes_custom.py +3 -9
- prediction_market_agent_tooling/tools/tokens/auto_deposit.py +1 -1
- prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
- prediction_market_agent_tooling/tools/web3_utils.py +9 -4
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/METADATA +8 -7
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/RECORD +36 -34
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -366
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/LICENSE +0 -0
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.68.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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"] = [
|
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.
|
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.
|
719
|
+
where_stms["historyHash"] = ZERO_BYTES.to_0x_hex()
|
718
720
|
else:
|
719
|
-
where_stms["historyHash_not"] = ZERO_BYTES.
|
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.
|
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.
|
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.
|
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.
|
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}"
|