prediction-market-agent-tooling 0.65.5__py3-none-any.whl → 0.69.17.dev1149__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 (88) hide show
  1. prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
  2. prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
  3. prediction_market_agent_tooling/abis/processor.abi.json +16 -0
  4. prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
  5. prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
  6. prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
  7. prediction_market_agent_tooling/benchmark/utils.py +13 -0
  8. prediction_market_agent_tooling/chains.py +1 -0
  9. prediction_market_agent_tooling/config.py +61 -2
  10. prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
  11. prediction_market_agent_tooling/deploy/agent.py +199 -67
  12. prediction_market_agent_tooling/deploy/agent_example.py +1 -1
  13. prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
  14. prediction_market_agent_tooling/deploy/constants.py +6 -0
  15. prediction_market_agent_tooling/gtypes.py +11 -1
  16. prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
  17. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
  18. prediction_market_agent_tooling/loggers.py +9 -1
  19. prediction_market_agent_tooling/logprobs_parser.py +2 -1
  20. prediction_market_agent_tooling/markets/agent_market.py +106 -18
  21. prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
  22. prediction_market_agent_tooling/markets/data_models.py +120 -7
  23. prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
  24. prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
  25. prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
  26. prediction_market_agent_tooling/markets/market_type.py +74 -0
  27. prediction_market_agent_tooling/markets/markets.py +7 -99
  28. prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
  29. prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
  30. prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
  31. prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
  32. prediction_market_agent_tooling/markets/omen/omen.py +112 -23
  33. prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
  34. prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
  35. prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
  36. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
  37. prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
  38. prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
  39. prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
  40. prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
  41. prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
  42. prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
  43. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
  44. prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
  45. prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
  46. prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
  47. prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
  48. prediction_market_agent_tooling/markets/seer/seer.py +393 -106
  49. prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
  50. prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
  51. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
  52. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
  53. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
  54. prediction_market_agent_tooling/tools/_generic_value.py +8 -2
  55. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
  56. prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
  57. prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
  58. prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
  59. prediction_market_agent_tooling/tools/contract.py +480 -38
  60. prediction_market_agent_tooling/tools/contract_utils.py +61 -0
  61. prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
  62. prediction_market_agent_tooling/tools/cow/models.py +122 -0
  63. prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
  64. prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
  65. prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
  66. prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
  67. prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
  68. prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
  69. prediction_market_agent_tooling/tools/openai_utils.py +31 -0
  70. prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
  71. prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
  72. prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
  73. prediction_market_agent_tooling/tools/rephrase.py +71 -0
  74. prediction_market_agent_tooling/tools/singleton.py +11 -6
  75. prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
  76. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
  77. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
  78. prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
  79. prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
  80. prediction_market_agent_tooling/tools/utils.py +61 -3
  81. prediction_market_agent_tooling/tools/web3_utils.py +63 -9
  82. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
  83. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
  84. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
  85. prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
  86. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
  87. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
  88. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info/licenses}/LICENSE +0 -0
@@ -1,5 +1,9 @@
1
+ import asyncio
1
2
  import typing as t
3
+ from datetime import timedelta
2
4
 
5
+ import cachetools
6
+ from cowdao_cowpy.common.api.errors import UnexpectedResponseError
3
7
  from eth_typing import ChecksumAddress
4
8
  from web3 import Web3
5
9
  from web3.types import TxReceipt
@@ -14,57 +18,96 @@ from prediction_market_agent_tooling.gtypes import (
14
18
  OutcomeStr,
15
19
  OutcomeToken,
16
20
  OutcomeWei,
21
+ Wei,
17
22
  xDai,
18
23
  )
19
24
  from prediction_market_agent_tooling.loggers import logger
20
25
  from prediction_market_agent_tooling.markets.agent_market import (
21
26
  AgentMarket,
27
+ ConditionalFilterType,
22
28
  FilterBy,
29
+ ParentMarket,
23
30
  ProcessedMarket,
24
- ProcessedTradedMarket,
31
+ QuestionType,
25
32
  SortBy,
26
33
  )
27
- from prediction_market_agent_tooling.markets.data_models import ExistingPosition
34
+ from prediction_market_agent_tooling.markets.blockchain_utils import store_trades
35
+ from prediction_market_agent_tooling.markets.data_models import (
36
+ ExistingPosition,
37
+ Resolution,
38
+ ResolvedBet,
39
+ )
28
40
  from prediction_market_agent_tooling.markets.market_fees import MarketFees
29
- from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
41
+ from prediction_market_agent_tooling.markets.omen.omen import (
42
+ OmenAgentMarket,
43
+ send_keeping_token_to_eoa_xdai,
44
+ )
45
+ from prediction_market_agent_tooling.markets.omen.omen_constants import (
46
+ SDAI_CONTRACT_ADDRESS,
47
+ )
48
+ from prediction_market_agent_tooling.markets.omen.omen_contracts import (
49
+ SeerAgentResultMappingContract,
50
+ )
30
51
  from prediction_market_agent_tooling.markets.seer.data_models import (
31
- RedeemParams,
32
52
  SeerMarket,
53
+ SeerMarketWithQuestions,
54
+ )
55
+ from prediction_market_agent_tooling.markets.seer.exceptions import (
56
+ PriceCalculationError,
33
57
  )
34
58
  from prediction_market_agent_tooling.markets.seer.price_manager import PriceManager
59
+ from prediction_market_agent_tooling.markets.seer.seer_api import get_seer_transactions
35
60
  from prediction_market_agent_tooling.markets.seer.seer_contracts import (
36
61
  GnosisRouter,
37
62
  SeerMarketFactory,
38
63
  )
39
64
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
65
+ SeerQuestionsCache,
40
66
  SeerSubgraphHandler,
67
+ TemplateId,
41
68
  )
42
69
  from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
43
70
  NewMarketEvent,
44
71
  )
72
+ from prediction_market_agent_tooling.markets.seer.swap_pool_handler import (
73
+ SwapPoolHandler,
74
+ )
45
75
  from prediction_market_agent_tooling.tools.contract import (
46
76
  ContractERC20OnGnosisChain,
47
77
  init_collateral_token_contract,
48
78
  to_gnosis_chain_contract,
49
79
  )
50
80
  from prediction_market_agent_tooling.tools.cow.cow_order import (
51
- get_buy_token_amount_else_raise,
52
- get_trades_by_owner,
81
+ NoLiquidityAvailableOnCowException,
82
+ OrderStatusError,
83
+ get_orders_by_owner,
84
+ get_trades_by_order_uid,
85
+ handle_allowance,
53
86
  swap_tokens_waiting,
87
+ wait_for_order_completion,
54
88
  )
55
89
  from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
56
90
  from prediction_market_agent_tooling.tools.tokens.auto_deposit import (
57
91
  auto_deposit_collateral_token,
58
92
  )
93
+ from prediction_market_agent_tooling.tools.tokens.slippage import (
94
+ get_slippage_tolerance_per_token,
95
+ )
59
96
  from prediction_market_agent_tooling.tools.tokens.usd import (
60
97
  get_token_in_usd,
61
98
  get_usd_in_token,
62
99
  )
100
+ from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow
63
101
 
64
102
  # We place a larger bet amount by default than Omen so that cow presents valid quotes.
65
103
  SEER_TINY_BET_AMOUNT = USD(0.1)
66
104
 
67
105
 
106
+ SHARED_CACHE: cachetools.TTLCache[t.Hashable, t.Any] = cachetools.TTLCache(
107
+ maxsize=256, ttl=10 * 60
108
+ )
109
+
110
+
68
111
  class SeerAgentMarket(AgentMarket):
69
112
  wrapped_tokens: list[ChecksumAddress]
70
113
  creator: HexAddress
@@ -74,6 +117,7 @@ class SeerAgentMarket(AgentMarket):
74
117
  None # Seer markets don't have a description, so just default to None.
75
118
  )
76
119
  outcomes_supply: int
120
+ minimum_market_liquidity_required: CollateralToken = CollateralToken(1)
77
121
 
78
122
  def get_collateral_token_contract(
79
123
  self, web3: Web3 | None = None
@@ -95,18 +139,53 @@ class SeerAgentMarket(AgentMarket):
95
139
 
96
140
  def store_trades(
97
141
  self,
98
- traded_market: ProcessedTradedMarket | None,
142
+ traded_market: ProcessedMarket | None,
99
143
  keys: APIKeys,
100
144
  agent_name: str,
101
145
  web3: Web3 | None = None,
102
146
  ) -> None:
103
- pass
147
+ return store_trades(
148
+ contract=SeerAgentResultMappingContract(),
149
+ market_id=Web3.to_checksum_address(self.id),
150
+ outcomes=self.outcomes,
151
+ traded_market=traded_market,
152
+ keys=keys,
153
+ agent_name=agent_name,
154
+ web3=web3,
155
+ )
156
+
157
+ def get_price_manager(self) -> PriceManager:
158
+ return PriceManager.build(HexBytes(HexStr(self.id)))
104
159
 
105
160
  def get_token_in_usd(self, x: CollateralToken) -> USD:
106
- return get_token_in_usd(x, self.collateral_token_contract_address_checksummed)
161
+ p = self.get_price_manager()
162
+ # This function is meant to convert market's collateral token into usd value, however, on Seer, market's collateral can be another's market outcome token.
163
+ # That's why we need this middle step.
164
+ sdai_amount = p.get_amount_of_collateral_in_token(
165
+ # Hard-coded SDAI, because Seer is atm hard-coded it as well, and it's needed in case of fallback to pools. CoW would work with other tokens as well.
166
+ SDAI_CONTRACT_ADDRESS,
167
+ x,
168
+ )
169
+ if sdai_amount is None:
170
+ raise RuntimeError(
171
+ "Both CoW and pool-fallback way of getting price failed."
172
+ )
173
+ return get_token_in_usd(sdai_amount.as_token, SDAI_CONTRACT_ADDRESS)
107
174
 
108
175
  def get_usd_in_token(self, x: USD) -> CollateralToken:
109
- return get_usd_in_token(x, self.collateral_token_contract_address_checksummed)
176
+ p = self.get_price_manager()
177
+ # This function is meant to convert market's collateral token into usd value, however, on Seer, market's collateral can be another's market outcome token.
178
+ # That's why we need this middle step.
179
+ token_amount = p.get_amount_of_token_in_collateral(
180
+ # Hard-coded SDAI, because Seer is atm hard-coded it as well, and it's needed in case of fallback to pools. CoW would work with other tokens as well.
181
+ SDAI_CONTRACT_ADDRESS,
182
+ OutcomeToken.from_token(get_usd_in_token(x, SDAI_CONTRACT_ADDRESS)),
183
+ )
184
+ if token_amount is None:
185
+ raise RuntimeError(
186
+ "Both CoW and pool-fallback way of getting price failed."
187
+ )
188
+ return token_amount
110
189
 
111
190
  def get_buy_token_amount(
112
191
  self, bet_amount: USD | CollateralToken, outcome_str: OutcomeStr
@@ -122,16 +201,15 @@ class SeerAgentMarket(AgentMarket):
122
201
 
123
202
  bet_amount_in_tokens = self.get_in_token(bet_amount)
124
203
 
125
- p = PriceManager.build(market_id=HexBytes(HexStr(self.id)))
126
- price = p.get_price_for_token(
204
+ p = self.get_price_manager()
205
+ amount_outcome_tokens = p.get_amount_of_collateral_in_token(
127
206
  token=outcome_token, collateral_exchange_amount=bet_amount_in_tokens
128
207
  )
129
- if not price:
208
+ if not amount_outcome_tokens:
130
209
  logger.info(f"Could not get price for token {outcome_token}")
131
210
  return None
132
211
 
133
- amount_outcome_tokens = bet_amount_in_tokens / price
134
- return OutcomeToken(amount_outcome_tokens)
212
+ return amount_outcome_tokens
135
213
 
136
214
  def get_sell_value_of_outcome_token(
137
215
  self, outcome: OutcomeStr, amount: OutcomeToken
@@ -141,13 +219,17 @@ class SeerAgentMarket(AgentMarket):
141
219
 
142
220
  wrapped_outcome_token = self.get_wrapped_token_for_outcome(outcome)
143
221
 
144
- # We calculate how much collateral we would get back if we sold `amount` of outcome token.
145
- value_outcome_token_in_collateral = get_buy_token_amount_else_raise(
146
- sell_amount=amount.as_outcome_wei.as_wei,
147
- sell_token=wrapped_outcome_token,
148
- buy_token=self.collateral_token_contract_address_checksummed,
222
+ p = self.get_price_manager()
223
+ value_outcome_token_in_collateral = p.get_amount_of_token_in_collateral(
224
+ wrapped_outcome_token, amount
149
225
  )
150
- return value_outcome_token_in_collateral.as_token
226
+
227
+ if value_outcome_token_in_collateral is None:
228
+ raise RuntimeError(
229
+ f"Could not get price for token from pools for {wrapped_outcome_token}"
230
+ )
231
+
232
+ return value_outcome_token_in_collateral
151
233
 
152
234
  @staticmethod
153
235
  def get_trade_balance(api_keys: APIKeys) -> USD:
@@ -156,9 +238,9 @@ class SeerAgentMarket(AgentMarket):
156
238
  def get_tiny_bet_amount(self) -> CollateralToken:
157
239
  return self.get_in_token(SEER_TINY_BET_AMOUNT)
158
240
 
159
- def get_position_else_raise(
241
+ def get_position(
160
242
  self, user_id: str, web3: Web3 | None = None
161
- ) -> ExistingPosition:
243
+ ) -> ExistingPosition | None:
162
244
  """
163
245
  Fetches position from the user in a given market.
164
246
  We ignore the INVALID balances since we are only interested in binary outcomes.
@@ -173,9 +255,11 @@ class SeerAgentMarket(AgentMarket):
173
255
  )
174
256
  )
175
257
 
176
- amounts_ot[
177
- OutcomeStr(outcome_str)
178
- ] = outcome_token_balance_wei.as_outcome_token
258
+ amounts_ot[outcome_str] = outcome_token_balance_wei.as_outcome_token
259
+
260
+ # Adhere to convenience from other markets, where we return None if user doesn't have any position.
261
+ if all(v == 0 for v in amounts_ot.values()):
262
+ return None
179
263
 
180
264
  amounts_current = {
181
265
  k: self.get_token_in_usd(self.get_sell_value_of_outcome_token(k, v))
@@ -191,33 +275,23 @@ class SeerAgentMarket(AgentMarket):
191
275
  amounts_ot=amounts_ot,
192
276
  )
193
277
 
194
- def get_position(
195
- self, user_id: str, web3: Web3 | None = None
196
- ) -> ExistingPosition | None:
197
- try:
198
- return self.get_position_else_raise(user_id=user_id, web3=web3)
199
- except Exception as e:
200
- logger.warning(f"Could not get position for user {user_id}, exception {e}")
201
- return None
202
-
203
- @staticmethod
204
- def get_user_id(api_keys: APIKeys) -> str:
205
- return OmenAgentMarket.get_user_id(api_keys)
206
-
207
278
  @staticmethod
208
279
  def _filter_markets_contained_in_trades(
209
280
  api_keys: APIKeys,
210
- markets: list[SeerMarket],
281
+ markets: t.Sequence[SeerMarket],
211
282
  ) -> list[SeerMarket]:
212
283
  """
213
284
  We filter the markets using previous trades by the user so that we don't have to process all Seer markets.
214
285
  """
215
- trades_by_user = get_trades_by_owner(api_keys.bet_from_address)
216
286
 
217
- traded_tokens = {t.buyToken for t in trades_by_user}.union(
218
- [t.sellToken for t in trades_by_user]
287
+ trades_by_user = get_seer_transactions(
288
+ api_keys.bet_from_address, RPCConfig().CHAIN_ID
219
289
  )
220
- filtered_markets = []
290
+
291
+ traded_tokens = {t.token_in_checksum for t in trades_by_user}.union(
292
+ [t.token_out_checksum for t in trades_by_user]
293
+ )
294
+ filtered_markets: list[SeerMarket] = []
221
295
  for market in markets:
222
296
  if any(
223
297
  [
@@ -230,10 +304,12 @@ class SeerAgentMarket(AgentMarket):
230
304
  return filtered_markets
231
305
 
232
306
  @staticmethod
233
- def redeem_winnings(api_keys: APIKeys) -> None:
234
- web3 = RPCConfig().get_web3()
307
+ def redeem_winnings(api_keys: APIKeys, web3: Web3 | None = None) -> None:
308
+ web3 = web3 or RPCConfig().get_web3()
235
309
  subgraph = SeerSubgraphHandler()
236
310
 
311
+ # ToDo - Find open positions by user directly
312
+
237
313
  closed_markets = subgraph.get_markets(
238
314
  filter_by=FilterBy.RESOLVED, sort_by=SortBy.NEWEST
239
315
  )
@@ -242,8 +318,8 @@ class SeerAgentMarket(AgentMarket):
242
318
  )
243
319
 
244
320
  market_balances = {
245
- market.id: market.get_outcome_token_balances(
246
- api_keys.bet_from_address, web3
321
+ market.id: list(
322
+ market.get_outcome_token_balances(api_keys.bet_from_address, web3)
247
323
  )
248
324
  for market in filtered_markets
249
325
  }
@@ -253,41 +329,157 @@ class SeerAgentMarket(AgentMarket):
253
329
  for market in filtered_markets
254
330
  if market.is_redeemable(owner=api_keys.bet_from_address, web3=web3)
255
331
  ]
332
+ logger.info(f"Got {len(markets_to_redeem)} markets to redeem on Seer.")
256
333
 
257
334
  gnosis_router = GnosisRouter()
258
335
  for market in markets_to_redeem:
259
336
  try:
260
- params = RedeemParams(
337
+ # GnosisRouter needs approval to use our outcome tokens
338
+ for i, token in enumerate(market.wrapped_tokens):
339
+ handle_allowance(
340
+ api_keys=api_keys,
341
+ sell_token=Web3.to_checksum_address(token),
342
+ amount_to_check_wei=market_balances[market.id][i].as_wei,
343
+ for_address=gnosis_router.address,
344
+ web3=web3,
345
+ )
346
+
347
+ # We can only ask for redeem of outcome tokens on correct outcomes
348
+ amounts_to_redeem = [
349
+ (amount if numerator > 0 else OutcomeWei(0))
350
+ for amount, numerator in zip(
351
+ market_balances[market.id], market.payout_numerators
352
+ )
353
+ ]
354
+
355
+ gnosis_router.redeem_to_base(
356
+ api_keys,
261
357
  market=Web3.to_checksum_address(market.id),
262
- outcome_indices=list(range(len(market.payout_numerators))),
263
- amounts=market_balances[market.id],
358
+ outcome_indexes=list(range(len(market.payout_numerators))),
359
+ amounts=amounts_to_redeem,
360
+ web3=web3,
361
+ )
362
+ logger.info(f"Redeemed market {market.url}.")
363
+ except Exception:
364
+ logger.exception(
365
+ f"Failed to redeem market {market.url}, {market.outcomes}, with amounts {market_balances[market.id]} and payout numerators {market.payout_numerators}, and wrapped tokens {market.wrapped_tokens}."
264
366
  )
265
- gnosis_router.redeem_to_base(api_keys, params=params, web3=web3)
266
- logger.info(f"Redeemed market {market.id.hex()}")
267
- except Exception as e:
268
- logger.error(f"Failed to redeem market {market.id.hex()}, {e}")
269
367
 
270
368
  # GnosisRouter withdraws sDai into wxDAI/xDai on its own, so no auto-withdraw needed by us.
271
369
 
370
+ def have_bet_on_market_since(self, keys: APIKeys, since: timedelta) -> bool:
371
+ """Check if the user has placed a bet on this market since a specific time using Cow API."""
372
+ # Cow endpoint doesn't allow us to filter by time.
373
+ start_time = utcnow() - since
374
+ prev_orders = get_orders_by_owner(owner=keys.bet_from_address)
375
+ for order in prev_orders:
376
+ if order.creationDate >= start_time and {
377
+ Web3.to_checksum_address(order.sellToken),
378
+ Web3.to_checksum_address(order.buyToken),
379
+ }.intersection(set(self.wrapped_tokens)):
380
+ return True
381
+
382
+ return False
383
+
384
+ def ensure_min_native_balance(
385
+ self,
386
+ min_required_balance: xDai,
387
+ multiplier: float = 3.0,
388
+ ) -> None:
389
+ send_keeping_token_to_eoa_xdai(
390
+ api_keys=APIKeys(),
391
+ min_required_balance=min_required_balance,
392
+ multiplier=multiplier,
393
+ )
394
+
272
395
  @staticmethod
273
396
  def verify_operational_balance(api_keys: APIKeys) -> bool:
274
397
  return OmenAgentMarket.verify_operational_balance(api_keys=api_keys)
275
398
 
399
+ @staticmethod
400
+ def build_resolution(
401
+ model: SeerMarketWithQuestions,
402
+ ) -> Resolution | None:
403
+ if model.questions[0].question.finalize_ts == 0:
404
+ # resolution not yet finalized
405
+ return None
406
+
407
+ if model.template_id != TemplateId.CATEGORICAL:
408
+ logger.warning("Resolution can only be built for categorical markets.")
409
+ # Future note - for scalar markets, simply fetch best_answer and convert
410
+ # from hex into int and divide by 1e18 (because Wei).
411
+ return None
412
+
413
+ if len(model.questions) != 1:
414
+ raise ValueError("Seer categorical markets must have 1 question.")
415
+
416
+ question = model.questions[0]
417
+ outcome = model.outcomes[int(question.question.best_answer.to_0x_hex(), 16)]
418
+ return Resolution(outcome=outcome, invalid=False)
419
+
420
+ @staticmethod
421
+ def convert_seer_market_into_market_with_questions(
422
+ seer_market: SeerMarket, seer_subgraph: SeerSubgraphHandler
423
+ ) -> "SeerMarketWithQuestions":
424
+ q = SeerQuestionsCache(seer_subgraph_handler=seer_subgraph)
425
+ q.fetch_questions([seer_market.id])
426
+ questions = q.market_id_to_questions[seer_market.id]
427
+ return SeerMarketWithQuestions(**seer_market.model_dump(), questions=questions)
428
+
429
+ @staticmethod
430
+ def get_parent(
431
+ model: SeerMarket,
432
+ seer_subgraph: SeerSubgraphHandler,
433
+ ) -> t.Optional["ParentMarket"]:
434
+ if not model.parent_market:
435
+ return None
436
+
437
+ # turn into a market with questions
438
+ parent_market_with_questions = (
439
+ SeerAgentMarket.convert_seer_market_into_market_with_questions(
440
+ model.parent_market, seer_subgraph=seer_subgraph
441
+ )
442
+ )
443
+
444
+ market_with_questions = check_not_none(
445
+ SeerAgentMarket.from_data_model_with_subgraph(
446
+ parent_market_with_questions, seer_subgraph, False
447
+ )
448
+ )
449
+
450
+ return ParentMarket(
451
+ market=market_with_questions, parent_outcome=model.parent_outcome
452
+ )
453
+
276
454
  @staticmethod
277
455
  def from_data_model_with_subgraph(
278
- model: SeerMarket, seer_subgraph: SeerSubgraphHandler
456
+ model: SeerMarketWithQuestions,
457
+ seer_subgraph: SeerSubgraphHandler,
458
+ must_have_prices: bool,
279
459
  ) -> t.Optional["SeerAgentMarket"]:
280
- p = PriceManager(seer_market=model, seer_subgraph=seer_subgraph)
281
-
282
- probability_map = p.build_probability_map()
283
- if not probability_map:
460
+ price_manager = PriceManager(seer_market=model, seer_subgraph=seer_subgraph)
461
+ wrapped_tokens = [Web3.to_checksum_address(i) for i in model.wrapped_tokens]
462
+ try:
463
+ (
464
+ probability_map,
465
+ outcome_token_pool,
466
+ ) = price_manager.build_initial_probs_from_pool(
467
+ model=model, wrapped_tokens=wrapped_tokens
468
+ )
469
+ except PriceCalculationError as e:
284
470
  logger.info(
285
- f"probability_map for market {model.id.hex()} could not be calculated. Skipping."
471
+ f"Error when calculating probabilities for market {model.id.to_0x_hex()} - {e}"
286
472
  )
287
- return None
473
+ if must_have_prices:
474
+ # Price calculation failed, so don't return the market
475
+ return None
288
476
 
289
- return SeerAgentMarket(
290
- id=model.id.hex(),
477
+ resolution = SeerAgentMarket.build_resolution(model=model)
478
+
479
+ parent = SeerAgentMarket.get_parent(model=model, seer_subgraph=seer_subgraph)
480
+
481
+ market = SeerAgentMarket(
482
+ id=model.id.to_0x_hex(),
291
483
  question=model.title,
292
484
  creator=model.creator,
293
485
  created_time=model.created_time,
@@ -296,15 +488,29 @@ class SeerAgentMarket(AgentMarket):
296
488
  condition_id=model.condition_id,
297
489
  url=model.url,
298
490
  close_time=model.close_time,
299
- wrapped_tokens=[Web3.to_checksum_address(i) for i in model.wrapped_tokens],
491
+ wrapped_tokens=wrapped_tokens,
300
492
  fees=MarketFees.get_zero_fees(),
301
- outcome_token_pool=None,
493
+ outcome_token_pool=outcome_token_pool,
302
494
  outcomes_supply=model.outcomes_supply,
303
- resolution=None,
495
+ resolution=resolution,
304
496
  volume=None,
305
497
  probabilities=probability_map,
498
+ upper_bound=model.upper_bound,
499
+ lower_bound=model.lower_bound,
500
+ parent=parent,
306
501
  )
307
502
 
503
+ return market
504
+
505
+ @staticmethod
506
+ def get_resolved_bets_made_since(
507
+ better_address: ChecksumAddress,
508
+ start_time: DatetimeUTC,
509
+ end_time: DatetimeUTC | None,
510
+ ) -> list[ResolvedBet]:
511
+ # TODO: https://github.com/gnosis/prediction-market-agent-tooling/issues/841
512
+ raise NotImplementedError()
513
+
308
514
  @staticmethod
309
515
  def get_markets(
310
516
  limit: int,
@@ -312,29 +518,40 @@ class SeerAgentMarket(AgentMarket):
312
518
  filter_by: FilterBy = FilterBy.OPEN,
313
519
  created_after: t.Optional[DatetimeUTC] = None,
314
520
  excluded_questions: set[str] | None = None,
315
- fetch_categorical_markets: bool = False,
521
+ question_type: QuestionType = QuestionType.ALL,
522
+ conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
316
523
  ) -> t.Sequence["SeerAgentMarket"]:
317
524
  seer_subgraph = SeerSubgraphHandler()
525
+
318
526
  markets = seer_subgraph.get_markets(
319
527
  limit=limit,
320
528
  sort_by=sort_by,
321
529
  filter_by=filter_by,
322
- include_categorical_markets=fetch_categorical_markets,
530
+ question_type=question_type,
531
+ conditional_filter_type=conditional_filter_type,
323
532
  )
324
533
 
325
534
  # We exclude the None values below because `from_data_model_with_subgraph` can return None, which
326
535
  # represents an invalid market.
327
- return [
536
+ seer_agent_markets = [
328
537
  market
329
538
  for m in markets
330
539
  if (
331
540
  market := SeerAgentMarket.from_data_model_with_subgraph(
332
- model=m, seer_subgraph=seer_subgraph
541
+ model=m,
542
+ seer_subgraph=seer_subgraph,
543
+ must_have_prices=filter_by == FilterBy.OPEN,
333
544
  )
334
545
  )
335
546
  is not None
336
547
  ]
337
548
 
549
+ if filter_by == FilterBy.OPEN:
550
+ # Extra manual filter for liquidity, as subgraph is sometimes unreliable.
551
+ seer_agent_markets = [m for m in seer_agent_markets if m.has_liquidity()]
552
+
553
+ return seer_agent_markets
554
+
338
555
  def get_outcome_str_from_idx(self, outcome_index: int) -> OutcomeStr:
339
556
  return self.outcomes[outcome_index]
340
557
 
@@ -353,7 +570,8 @@ class SeerAgentMarket(AgentMarket):
353
570
  f"Could not fetch pool for token {outcome_token}, no liquidity available for outcome."
354
571
  )
355
572
  return CollateralToken(0)
356
- p = PriceManager.build(HexBytes(HexStr(self.id)))
573
+
574
+ p = self.get_price_manager()
357
575
  total = CollateralToken(0)
358
576
 
359
577
  for token_address in [pool.token0.id, pool.token1.id]:
@@ -363,22 +581,18 @@ class SeerAgentMarket(AgentMarket):
363
581
  )
364
582
 
365
583
  token_balance = token_contract.balance_of_in_tokens(
366
- for_address=Web3.to_checksum_address(HexAddress(HexStr(pool.id.hex()))),
584
+ for_address=Web3.to_checksum_address(
585
+ HexAddress(HexStr(pool.id.to_0x_hex()))
586
+ ),
367
587
  web3=web3,
368
588
  )
369
-
370
- # get price
371
- token_price_in_sdai = (
372
- p.get_token_price_from_pools(token=token_address_checksummed)
373
- if token_address_checksummed
374
- != self.collateral_token_contract_address_checksummed
375
- else CollateralToken(1.0)
589
+ collateral_balance = p.get_amount_of_token_in_collateral(
590
+ token_address_checksummed, OutcomeToken.from_token(token_balance)
376
591
  )
377
592
 
378
593
  # We ignore the liquidity in outcome tokens if price unknown.
379
- if token_price_in_sdai:
380
- sdai_balance = token_balance * token_price_in_sdai
381
- total += sdai_balance
594
+ if collateral_balance:
595
+ total += collateral_balance
382
596
 
383
597
  return total
384
598
 
@@ -393,8 +607,9 @@ class SeerAgentMarket(AgentMarket):
393
607
 
394
608
  def has_liquidity_for_outcome(self, outcome: OutcomeStr) -> bool:
395
609
  liquidity = self.get_liquidity_for_outcome(outcome)
396
- return liquidity > CollateralToken(0)
610
+ return liquidity > self.minimum_market_liquidity_required
397
611
 
612
+ @cachetools.cached(cache=SHARED_CACHE, key=lambda self: f"has_liquidity_{self.id}")
398
613
  def has_liquidity(self) -> bool:
399
614
  # We define a market as having liquidity if it has liquidity for all outcomes except for the invalid (index -1)
400
615
  return all(
@@ -405,6 +620,85 @@ class SeerAgentMarket(AgentMarket):
405
620
  outcome_idx = self.outcomes.index(outcome)
406
621
  return self.wrapped_tokens[outcome_idx]
407
622
 
623
+ def _swap_tokens_with_fallback(
624
+ self,
625
+ sell_token: ChecksumAddress,
626
+ buy_token: ChecksumAddress,
627
+ amount_wei: Wei,
628
+ api_keys: APIKeys,
629
+ web3: Web3 | None,
630
+ ) -> str:
631
+ """
632
+ Helper method to swap tokens with a fallback to direct pool swapping if the order times out.
633
+
634
+ Args:
635
+ sell_token: Address of the token to sell
636
+ buy_token: Address of the token to buy
637
+ amount_wei: Amount to swap in wei
638
+ api_keys: API keys for the transaction
639
+ web3: Web3 instance
640
+
641
+ Returns:
642
+ Transaction hash of the successful swap
643
+ """
644
+ slippage_tolerance = get_slippage_tolerance_per_token(sell_token, buy_token)
645
+ try:
646
+ _, order = swap_tokens_waiting(
647
+ amount_wei=amount_wei,
648
+ sell_token=sell_token,
649
+ buy_token=buy_token,
650
+ api_keys=api_keys,
651
+ web3=web3,
652
+ wait_order_complete=False,
653
+ timeout=timedelta(minutes=2),
654
+ slippage_tolerance=slippage_tolerance,
655
+ )
656
+ order_metadata = asyncio.run(wait_for_order_completion(order=order))
657
+ logger.info(
658
+ f"Swapped {sell_token} for {buy_token}. Order details {order_metadata}"
659
+ )
660
+ trades = get_trades_by_order_uid(HexBytes(order_metadata.uid.root))
661
+ if len(trades) != 1:
662
+ raise ValueError(
663
+ f"Expected exactly 1 trade from {order_metadata=}, but got {len(trades)=}."
664
+ )
665
+ cow_tx_hash = trades[0].txHash
666
+ logger.info(f"TxHash is {cow_tx_hash=} for {order_metadata.uid.root=}.")
667
+ return cow_tx_hash.to_0x_hex()
668
+
669
+ except (
670
+ UnexpectedResponseError,
671
+ TimeoutError,
672
+ NoLiquidityAvailableOnCowException,
673
+ OrderStatusError,
674
+ ) as e:
675
+ # We don't retry if not enough balance.
676
+ if "InsufficientBalance" in str(e):
677
+ raise e
678
+ # Note that we don't need to cancel the order because we are setting
679
+ # timeout and valid_to in the order, thus the order simply expires.
680
+ logger.info(
681
+ f"Exception occured when swapping tokens via Cowswap, doing swap via pools. {e}"
682
+ )
683
+
684
+ if not self.has_liquidity():
685
+ logger.error(f"Market {self.id} has no liquidity. Cannot place bet.")
686
+ raise e
687
+
688
+ tx_receipt = SwapPoolHandler(
689
+ api_keys=api_keys,
690
+ market_id=self.id,
691
+ collateral_token_address=self.collateral_token_contract_address_checksummed,
692
+ ).buy_or_sell_outcome_token(
693
+ token_in=sell_token,
694
+ token_out=buy_token,
695
+ amount_in=amount_wei,
696
+ web3=web3,
697
+ )
698
+ swap_pool_tx_hash = tx_receipt["transactionHash"]
699
+ logger.info(f"TxHash is {swap_pool_tx_hash=}.")
700
+ return swap_pool_tx_hash.to_0x_hex()
701
+
408
702
  def place_bet(
409
703
  self,
410
704
  outcome: OutcomeStr,
@@ -413,6 +707,7 @@ class SeerAgentMarket(AgentMarket):
413
707
  web3: Web3 | None = None,
414
708
  api_keys: APIKeys | None = None,
415
709
  ) -> str:
710
+ outcome_token = self.get_wrapped_token_for_outcome(outcome)
416
711
  api_keys = api_keys if api_keys is not None else APIKeys()
417
712
  if not self.can_be_traded():
418
713
  raise ValueError(
@@ -428,27 +723,13 @@ class SeerAgentMarket(AgentMarket):
428
723
  collateral_contract, amount_wei, api_keys, web3
429
724
  )
430
725
 
431
- collateral_balance = collateral_contract.balanceOf(api_keys.bet_from_address)
432
- if collateral_balance < amount_wei:
433
- raise ValueError(
434
- f"Balance {collateral_balance} not enough for bet size {amount}"
435
- )
436
-
437
- outcome_token = self.get_wrapped_token_for_outcome(outcome)
438
-
439
- # Sell sDAI using token address
440
- order_metadata = swap_tokens_waiting(
441
- amount_wei=amount_wei,
726
+ return self._swap_tokens_with_fallback(
442
727
  sell_token=collateral_contract.address,
443
728
  buy_token=outcome_token,
729
+ amount_wei=amount_wei,
444
730
  api_keys=api_keys,
445
731
  web3=web3,
446
732
  )
447
- logger.debug(
448
- f"Purchased {outcome_token} in exchange for {collateral_contract.address}. Order details {order_metadata}"
449
- )
450
-
451
- return order_metadata.uid.root
452
733
 
453
734
  def sell_tokens(
454
735
  self,
@@ -470,21 +751,27 @@ class SeerAgentMarket(AgentMarket):
470
751
  else self.get_in_token(amount).as_wei
471
752
  )
472
753
 
473
- order_metadata = swap_tokens_waiting(
474
- amount_wei=token_amount,
754
+ return self._swap_tokens_with_fallback(
475
755
  sell_token=outcome_token,
476
756
  buy_token=Web3.to_checksum_address(
477
757
  self.collateral_token_contract_address_checksummed
478
758
  ),
759
+ amount_wei=token_amount,
479
760
  api_keys=api_keys,
480
761
  web3=web3,
481
762
  )
482
763
 
483
- logger.debug(
484
- f"Sold {outcome_token} in exchange for {self.collateral_token_contract_address_checksummed}. Order details {order_metadata}"
764
+ def get_token_balance(
765
+ self, user_id: str, outcome: OutcomeStr, web3: Web3 | None = None
766
+ ) -> OutcomeToken:
767
+ erc20_token = ContractERC20OnGnosisChain(
768
+ address=self.get_wrapped_token_for_outcome(outcome)
769
+ )
770
+ return OutcomeToken.from_token(
771
+ erc20_token.balance_of_in_tokens(
772
+ for_address=Web3.to_checksum_address(user_id), web3=web3
773
+ )
485
774
  )
486
-
487
- return order_metadata.uid.root
488
775
 
489
776
 
490
777
  def seer_create_market_tx(