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
@@ -6,11 +6,12 @@ from enum import Enum
6
6
  from functools import cached_property
7
7
 
8
8
  from pydantic import computed_field
9
+ from pydantic_ai.exceptions import UnexpectedModelBehavior
9
10
 
10
11
  from prediction_market_agent_tooling.config import APIKeys
11
12
  from prediction_market_agent_tooling.deploy.betting_strategy import (
12
13
  BettingStrategy,
13
- MultiCategoricalMaxAccuracyBettingStrategy,
14
+ CategoricalMaxAccuracyBettingStrategy,
14
15
  TradeType,
15
16
  )
16
17
  from prediction_market_agent_tooling.deploy.trade_interval import (
@@ -21,9 +22,10 @@ from prediction_market_agent_tooling.gtypes import USD, OutcomeToken, xDai
21
22
  from prediction_market_agent_tooling.loggers import logger
22
23
  from prediction_market_agent_tooling.markets.agent_market import (
23
24
  AgentMarket,
25
+ ConditionalFilterType,
24
26
  FilterBy,
25
27
  ProcessedMarket,
26
- ProcessedTradedMarket,
28
+ QuestionType,
27
29
  SortBy,
28
30
  )
29
31
  from prediction_market_agent_tooling.markets.data_models import (
@@ -31,15 +33,10 @@ from prediction_market_agent_tooling.markets.data_models import (
31
33
  ExistingPosition,
32
34
  PlacedTrade,
33
35
  ProbabilisticAnswer,
36
+ ScalarProbabilisticAnswer,
34
37
  Trade,
35
38
  )
36
- from prediction_market_agent_tooling.markets.markets import (
37
- MarketType,
38
- have_bet_on_market_since,
39
- )
40
- from prediction_market_agent_tooling.markets.omen.omen import (
41
- send_keeping_token_to_eoa_xdai,
42
- )
39
+ from prediction_market_agent_tooling.markets.market_type import MarketType
43
40
  from prediction_market_agent_tooling.tools.custom_exceptions import (
44
41
  CantPayForGasError,
45
42
  OutOfFundsError,
@@ -47,12 +44,16 @@ from prediction_market_agent_tooling.tools.custom_exceptions import (
47
44
  from prediction_market_agent_tooling.tools.is_invalid import is_invalid
48
45
  from prediction_market_agent_tooling.tools.is_predictable import is_predictable_binary
49
46
  from prediction_market_agent_tooling.tools.langfuse_ import langfuse_context, observe
47
+ from prediction_market_agent_tooling.tools.rephrase import (
48
+ rephrase_question_to_unconditional,
49
+ )
50
50
  from prediction_market_agent_tooling.tools.tokens.main_token import (
51
51
  MINIMUM_NATIVE_TOKEN_IN_EOA_FOR_FEES,
52
52
  )
53
53
  from prediction_market_agent_tooling.tools.utils import (
54
54
  DatetimeUTC,
55
55
  check_not_none,
56
+ retry_until_true,
56
57
  utcnow,
57
58
  )
58
59
 
@@ -196,6 +197,7 @@ class DeployablePredictionAgent(DeployableAgent):
196
197
  trade_on_markets_created_after: DatetimeUTC | None = None
197
198
  get_markets_sort_by: SortBy = SortBy.CLOSING_SOONEST
198
199
  get_markets_filter_by: FilterBy = FilterBy.OPEN
200
+ rephrase_conditional_markets: bool = True
199
201
 
200
202
  # Agent behaviour when filtering fetched markets
201
203
  allow_invalid_questions: bool = False
@@ -205,6 +207,8 @@ class DeployablePredictionAgent(DeployableAgent):
205
207
  MINIMUM_NATIVE_TOKEN_IN_EOA_FOR_FEES
206
208
  )
207
209
 
210
+ just_warn_on_unexpected_model_behavior: bool = False
211
+
208
212
  # Only Metaculus allows to post predictions without trading (buying/selling of outcome tokens).
209
213
  supported_markets: t.Sequence[MarketType] = [MarketType.METACULUS]
210
214
 
@@ -219,11 +223,12 @@ class DeployablePredictionAgent(DeployableAgent):
219
223
  def initialize_langfuse(self) -> None:
220
224
  super().initialize_langfuse()
221
225
  # Auto-observe all the methods where it makes sense, so that subclassses don't need to do it manually.
222
- self.have_bet_on_market_since = observe()(self.have_bet_on_market_since) # type: ignore[method-assign]
223
226
  self.verify_market = observe()(self.verify_market) # type: ignore[method-assign]
224
227
  self.answer_binary_market = observe()(self.answer_binary_market) # type: ignore[method-assign]
225
228
  self.answer_categorical_market = observe()(self.answer_categorical_market) # type: ignore[method-assign]
229
+ self.answer_scalar_market = observe()(self.answer_scalar_market) # type: ignore[method-assign]
226
230
  self.process_market = observe()(self.process_market) # type: ignore[method-assign]
231
+ self.rephrase_market_to_unconditional = observe()(self.rephrase_market_to_unconditional) # type: ignore[method-assign]
227
232
 
228
233
  def update_langfuse_trace_by_market(
229
234
  self, market_type: MarketType, market: AgentMarket
@@ -265,16 +270,13 @@ class DeployablePredictionAgent(DeployableAgent):
265
270
  f"{api_keys=} doesn't have enough operational balance."
266
271
  )
267
272
 
268
- def have_bet_on_market_since(self, market: AgentMarket, since: timedelta) -> bool:
269
- return have_bet_on_market_since(keys=APIKeys(), market=market, since=since)
270
-
271
273
  def verify_market(self, market_type: MarketType, market: AgentMarket) -> bool:
272
274
  """
273
275
  Subclasses can implement their own logic instead of this one, or on top of this one.
274
276
  By default, it allows only markets where user didn't bet recently and it's a reasonable question.
275
277
  """
276
- if self.have_bet_on_market_since(
277
- market, since=self.same_market_trade_interval.get(market=market)
278
+ if market.have_bet_on_market_since(
279
+ keys=APIKeys(), since=self.same_market_trade_interval.get(market=market)
278
280
  ):
279
281
  logger.info(
280
282
  f"Market already bet on within {self.same_market_trade_interval}."
@@ -301,11 +303,44 @@ class DeployablePredictionAgent(DeployableAgent):
301
303
 
302
304
  return True
303
305
 
306
+ def rephrase_market_to_unconditional(
307
+ self,
308
+ market_: AgentMarket,
309
+ ) -> AgentMarket:
310
+ """
311
+ If `rephrase_conditional_markets` is set to True,
312
+ this method will be used to rephrase the question to account for the parent's market probability in the agent's decision process.
313
+ """
314
+ new = market_.model_copy(deep=True)
315
+
316
+ if new.parent is not None and new.parent.market.parent is not None:
317
+ new.parent.market = self.rephrase_market_to_unconditional(new.parent.market)
318
+
319
+ rephrased_question = (
320
+ rephrase_question_to_unconditional(
321
+ new.question,
322
+ new.parent.market.question,
323
+ new.parent.market.outcomes[new.parent.parent_outcome],
324
+ )
325
+ if new.parent is not None
326
+ else new.question
327
+ )
328
+
329
+ new.question = rephrased_question
330
+ new.parent = None
331
+
332
+ return new
333
+
304
334
  def answer_categorical_market(
305
335
  self, market: AgentMarket
306
336
  ) -> CategoricalProbabilisticAnswer | None:
307
337
  raise NotImplementedError("This method must be implemented by the subclass")
308
338
 
339
+ def answer_scalar_market(
340
+ self, market: AgentMarket
341
+ ) -> ScalarProbabilisticAnswer | None:
342
+ raise NotImplementedError("This method must be implemented by the subclass")
343
+
309
344
  def answer_binary_market(self, market: AgentMarket) -> ProbabilisticAnswer | None:
310
345
  """
311
346
  Answer the binary market.
@@ -328,6 +363,28 @@ class DeployablePredictionAgent(DeployableAgent):
328
363
  return True
329
364
  return False
330
365
 
366
+ @property
367
+ def fetch_scalar_markets(self) -> bool:
368
+ # Check if the subclass has implemented the answer_scalar_market method, if yes, fetch scalar markets as well.
369
+ if self.answer_scalar_market.__wrapped__.__func__ is not DeployablePredictionAgent.answer_scalar_market: # type: ignore[attr-defined] # This works just fine, but mypy doesn't know about it for some reason.
370
+ return True
371
+ return False
372
+
373
+ @property
374
+ def conditional_filter_type(self) -> ConditionalFilterType:
375
+ if self.rephrase_conditional_markets:
376
+ return ConditionalFilterType.ALL
377
+ return ConditionalFilterType.ONLY_NOT_CONDITIONAL
378
+
379
+ @property
380
+ def agent_question_type(self) -> QuestionType:
381
+ if self.fetch_scalar_markets:
382
+ return QuestionType.SCALAR
383
+ elif self.fetch_categorical_markets:
384
+ return QuestionType.CATEGORICAL
385
+ else:
386
+ return QuestionType.BINARY
387
+
331
388
  def get_markets(
332
389
  self,
333
390
  market_type: MarketType,
@@ -336,13 +393,15 @@ class DeployablePredictionAgent(DeployableAgent):
336
393
  Override this method to customize what markets will fetch for processing.
337
394
  """
338
395
  cls = market_type.market_class
396
+
339
397
  # Fetch the soonest closing markets to choose from
340
398
  available_markets = cls.get_markets(
341
399
  limit=self.n_markets_to_fetch,
342
400
  sort_by=self.get_markets_sort_by,
343
401
  filter_by=self.get_markets_filter_by,
344
402
  created_after=self.trade_on_markets_created_after,
345
- fetch_categorical_markets=self.fetch_categorical_markets,
403
+ question_type=self.agent_question_type,
404
+ conditional_filter_type=self.conditional_filter_type,
346
405
  )
347
406
  return available_markets
348
407
 
@@ -352,15 +411,13 @@ class DeployablePredictionAgent(DeployableAgent):
352
411
  """
353
412
  Executed before processing of each market.
354
413
  """
355
- api_keys = APIKeys()
356
414
 
357
415
  if market_type.is_blockchain_market:
358
- # Exchange wxdai back to xdai if the balance is getting low, so we can keep paying for fees.
416
+ # Ensure we have enough native token balance for transaction fees
359
417
  if self.min_balance_to_keep_in_native_currency is not None:
360
- send_keeping_token_to_eoa_xdai(
361
- api_keys,
418
+ market.ensure_min_native_balance(
362
419
  min_required_balance=self.min_balance_to_keep_in_native_currency,
363
- multiplier=3,
420
+ multiplier=3.0,
364
421
  )
365
422
 
366
423
  def build_answer(
@@ -375,12 +432,16 @@ class DeployablePredictionAgent(DeployableAgent):
375
432
 
376
433
  logger.info(f"Answering market '{market.question}'.")
377
434
 
435
+ if self.rephrase_conditional_markets and market.parent is not None:
436
+ market = self.rephrase_market_to_unconditional(market)
437
+
378
438
  if market.is_binary:
379
439
  try:
380
440
  binary_answer = self.answer_binary_market(market)
381
441
  return (
382
442
  CategoricalProbabilisticAnswer.from_probabilistic_answer(
383
- binary_answer
443
+ binary_answer,
444
+ market.outcomes,
384
445
  )
385
446
  if binary_answer is not None
386
447
  else None
@@ -389,9 +450,39 @@ class DeployablePredictionAgent(DeployableAgent):
389
450
  logger.info(
390
451
  "answer_binary_market() not implemented, falling back to answer_categorical_market()"
391
452
  )
392
-
453
+ elif market.is_scalar:
454
+ scalar_answer = self.answer_scalar_market(market)
455
+ return (
456
+ CategoricalProbabilisticAnswer.from_scalar_answer(
457
+ scalar_answer,
458
+ market.outcomes,
459
+ )
460
+ if scalar_answer is not None
461
+ else None
462
+ )
393
463
  return self.answer_categorical_market(market)
394
464
 
465
+ def verify_answer_outcomes(
466
+ self, market: AgentMarket, answer: CategoricalProbabilisticAnswer
467
+ ) -> None:
468
+ outcomes_from_prob_map = list(answer.probabilities.keys())
469
+
470
+ if any(
471
+ outcome_from_answer not in market.outcomes
472
+ for outcome_from_answer in outcomes_from_prob_map
473
+ ):
474
+ raise ValueError(
475
+ f"Some of generated outcomes ({outcomes_from_prob_map=}) in probability map doesn't match with market's outcomes ({market.outcomes=})."
476
+ )
477
+
478
+ if any(
479
+ market_outcome not in outcomes_from_prob_map
480
+ for market_outcome in market.outcomes
481
+ ):
482
+ logger.warning(
483
+ f"Some of market's outcomes ({market.outcomes=}) isn't included in the probability map ({outcomes_from_prob_map=})."
484
+ )
485
+
395
486
  def process_market(
396
487
  self,
397
488
  market_type: MarketType,
@@ -403,17 +494,28 @@ class DeployablePredictionAgent(DeployableAgent):
403
494
  f"Processing market {market.question=} from {market.url=} with liquidity {market.get_liquidity()}."
404
495
  )
405
496
 
406
- answer = self.build_answer(
407
- market=market, market_type=market_type, verify_market=verify_market
408
- )
497
+ try:
498
+ answer = self.build_answer(
499
+ market=market, market_type=market_type, verify_market=verify_market
500
+ )
501
+ except UnexpectedModelBehavior:
502
+ (
503
+ logger.warning
504
+ if self.just_warn_on_unexpected_model_behavior
505
+ else logger.exception
506
+ )(f"Unexpected model behaviour in {self.__class__.__name__}.")
507
+ answer = None
508
+
509
+ if answer is not None:
510
+ self.verify_answer_outcomes(market=market, answer=answer)
409
511
 
410
512
  processed_market = (
411
- ProcessedMarket(answer=answer) if answer is not None else None
513
+ ProcessedMarket(answer=answer, trades=[]) if answer is not None else None
412
514
  )
413
515
 
414
516
  self.update_langfuse_trace_by_processed_market(market_type, processed_market)
415
517
  logger.info(
416
- f"Processed market {market.question=} from {market.url=} with {answer=}."
518
+ f"Processed market {market.question=} from {market.url=} with {processed_market=}."
417
519
  )
418
520
  return processed_market
419
521
 
@@ -523,12 +625,13 @@ class DeployableTraderAgent(DeployablePredictionAgent):
523
625
  super().initialize_langfuse()
524
626
  # Auto-observe all the methods where it makes sense, so that subclassses don't need to do it manually.
525
627
  self.build_trades = observe()(self.build_trades) # type: ignore[method-assign]
628
+ self.execute_trades = observe()(self.execute_trades) # type: ignore[method-assign]
526
629
 
527
630
  def check_min_required_balance_to_trade(self, market: AgentMarket) -> None:
528
631
  api_keys = APIKeys()
529
632
 
530
633
  # Get the strategy to know how much it will bet.
531
- strategy = self.get_betting_strategy(market)
634
+ strategy = self.get_betting_strategy_supported(market)
532
635
  # Have a little bandwidth after the bet.
533
636
  min_required_balance_to_trade = strategy.maximum_possible_bet_amount * 1.01
534
637
 
@@ -543,8 +646,10 @@ class DeployableTraderAgent(DeployablePredictionAgent):
543
646
 
544
647
  total_amount = market.get_in_usd(market.get_tiny_bet_amount())
545
648
  existing_position = market.get_position(user_id=user_id)
649
+
546
650
  if existing_position and existing_position.total_amount_current > USD(0):
547
651
  total_amount += existing_position.total_amount_current
652
+
548
653
  return total_amount
549
654
 
550
655
  def get_betting_strategy(self, market: AgentMarket) -> BettingStrategy:
@@ -554,7 +659,18 @@ class DeployableTraderAgent(DeployablePredictionAgent):
554
659
  Given the market and prediction, agent uses this method to calculate optimal outcome and bet size.
555
660
  """
556
661
  total_amount = self.get_total_amount_to_bet(market)
557
- return MultiCategoricalMaxAccuracyBettingStrategy(bet_amount=total_amount)
662
+ return CategoricalMaxAccuracyBettingStrategy(max_position_amount=total_amount)
663
+
664
+ def get_betting_strategy_supported(self, market: AgentMarket) -> BettingStrategy:
665
+ """
666
+ Use this class internally to assert that the configured betting strategy works with the given market.
667
+ """
668
+ strategy = self.get_betting_strategy(market=market)
669
+ if not strategy.is_market_supported(market):
670
+ raise ValueError(
671
+ f"Market {market.url} is not supported by the strategy {strategy}."
672
+ )
673
+ return strategy
558
674
 
559
675
  def build_trades(
560
676
  self,
@@ -562,36 +678,13 @@ class DeployableTraderAgent(DeployablePredictionAgent):
562
678
  answer: CategoricalProbabilisticAnswer,
563
679
  existing_position: ExistingPosition | None,
564
680
  ) -> list[Trade]:
565
- strategy = self.get_betting_strategy(market=market)
681
+ strategy = self.get_betting_strategy_supported(market=market)
566
682
  trades = strategy.calculate_trades(existing_position, answer, market)
567
683
  return trades
568
684
 
569
- def before_process_market(
570
- self, market_type: MarketType, market: AgentMarket
571
- ) -> None:
572
- super().before_process_market(market_type, market)
573
- self.check_min_required_balance_to_trade(market)
574
-
575
- def process_market(
576
- self,
577
- market_type: MarketType,
578
- market: AgentMarket,
579
- verify_market: bool = True,
580
- ) -> ProcessedTradedMarket | None:
581
- processed_market = super().process_market(market_type, market, verify_market)
582
- if processed_market is None:
583
- return None
584
-
585
- api_keys = APIKeys()
586
- user_id = market.get_user_id(api_keys=api_keys)
587
-
588
- existing_position = market.get_position(user_id=user_id)
589
- trades = self.build_trades(
590
- market=market,
591
- answer=processed_market.answer,
592
- existing_position=existing_position,
593
- )
594
-
685
+ def execute_trades(
686
+ self, market: AgentMarket, trades: list[Trade]
687
+ ) -> list[PlacedTrade]:
595
688
  # It can take quite some time before agent processes all the markets, recheck here if the market didn't get closed in the meantime, to not error out completely.
596
689
  # Unfortunately, we can not just add some room into closing time of the market while fetching them, because liquidity can be removed at any time by the liquidity providers.
597
690
  still_tradeable = market.can_be_traded()
@@ -600,7 +693,8 @@ class DeployableTraderAgent(DeployablePredictionAgent):
600
693
  f"Market {market.question=} ({market.url}) was selected to processing, but is not tradeable anymore."
601
694
  )
602
695
 
603
- placed_trades = []
696
+ placed_trades: list[PlacedTrade] = []
697
+
604
698
  for trade in trades:
605
699
  logger.info(f"Executing trade {trade} on market {market.id} ({market.url})")
606
700
 
@@ -612,8 +706,13 @@ class DeployableTraderAgent(DeployablePredictionAgent):
612
706
  )
613
707
  case TradeType.SELL:
614
708
  # Get actual value of the position we are going to sell, and if it's less than we wanted to sell, simply sell all of it.
709
+ # In this palce, we expect to have positions, so retry a few times if None are returned, which sometimes happens due to flaky subgraph.
615
710
  current_position = check_not_none(
616
- market.get_position(user_id),
711
+ retry_until_true(
712
+ lambda x: x is not None and x.total_amount_ot > 0
713
+ )(market.get_position)(
714
+ market.get_user_id(api_keys=self.api_keys)
715
+ ),
617
716
  "Should exists if we are going to sell outcomes.",
618
717
  )
619
718
 
@@ -641,7 +740,40 @@ class DeployableTraderAgent(DeployablePredictionAgent):
641
740
  f"Trade execution skipped because, {self.place_trades=} or {still_tradeable=}."
642
741
  )
643
742
 
644
- traded_market = ProcessedTradedMarket(
743
+ return placed_trades
744
+
745
+ def before_process_market(
746
+ self, market_type: MarketType, market: AgentMarket
747
+ ) -> None:
748
+ super().before_process_market(market_type, market)
749
+ self.check_min_required_balance_to_trade(market)
750
+
751
+ def process_market(
752
+ self,
753
+ market_type: MarketType,
754
+ market: AgentMarket,
755
+ verify_market: bool = True,
756
+ ) -> ProcessedMarket | None:
757
+ processed_market = super().process_market(market_type, market, verify_market)
758
+ if processed_market is None:
759
+ return None
760
+
761
+ user_id = market.get_user_id(api_keys=self.api_keys)
762
+
763
+ try:
764
+ existing_position = market.get_position(user_id=user_id)
765
+ except Exception as e:
766
+ logger.warning(f"Could not get position for user {user_id}, exception {e}")
767
+ return None
768
+
769
+ trades = self.build_trades(
770
+ market=market,
771
+ answer=processed_market.answer,
772
+ existing_position=existing_position,
773
+ )
774
+ placed_trades = self.execute_trades(market, trades)
775
+
776
+ traded_market = ProcessedMarket(
645
777
  answer=processed_market.answer, trades=placed_trades
646
778
  )
647
779
  logger.info(f"Traded market {market.question=} from {market.url=}.")
@@ -659,10 +791,10 @@ class DeployableTraderAgent(DeployablePredictionAgent):
659
791
  market,
660
792
  processed_market,
661
793
  )
662
- if isinstance(processed_market, ProcessedTradedMarket):
663
- if self.store_trades:
664
- market.store_trades(processed_market, api_keys, self.agent_name)
665
- else:
666
- logger.info(
667
- f"Trades {processed_market.trades} not stored because {self.store_trades=}."
668
- )
794
+
795
+ if self.store_trades and processed_market is not None:
796
+ market.store_trades(processed_market, api_keys, self.agent_name)
797
+ else:
798
+ logger.info(
799
+ f"Trades {processed_market=} not stored because {self.store_trades=}."
800
+ )
@@ -6,7 +6,7 @@ from prediction_market_agent_tooling.markets.agent_market import AgentMarket, So
6
6
  from prediction_market_agent_tooling.markets.data_models import (
7
7
  CategoricalProbabilisticAnswer,
8
8
  )
9
- from prediction_market_agent_tooling.markets.markets import MarketType
9
+ from prediction_market_agent_tooling.markets.market_type import MarketType
10
10
 
11
11
 
12
12
  class DeployableCoinFlipAgent(DeployableTraderAgent):