prediction-market-agent-tooling 0.56.0.dev1863__py3-none-any.whl → 0.56.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. prediction_market_agent_tooling/deploy/agent.py +23 -12
  2. prediction_market_agent_tooling/jobs/jobs_models.py +27 -2
  3. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +67 -41
  4. prediction_market_agent_tooling/markets/agent_market.py +8 -2
  5. prediction_market_agent_tooling/markets/base_subgraph_handler.py +51 -0
  6. prediction_market_agent_tooling/markets/markets.py +12 -0
  7. prediction_market_agent_tooling/markets/metaculus/metaculus.py +1 -1
  8. prediction_market_agent_tooling/markets/omen/data_models.py +11 -2
  9. prediction_market_agent_tooling/markets/omen/omen.py +16 -9
  10. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +29 -51
  11. prediction_market_agent_tooling/markets/seer/data_models.py +27 -0
  12. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +142 -0
  13. prediction_market_agent_tooling/tools/caches/db_cache.py +30 -62
  14. prediction_market_agent_tooling/tools/is_invalid.py +1 -1
  15. prediction_market_agent_tooling/tools/utils.py +2 -0
  16. {prediction_market_agent_tooling-0.56.0.dev1863.dist-info → prediction_market_agent_tooling-0.56.1.dist-info}/METADATA +1 -1
  17. {prediction_market_agent_tooling-0.56.0.dev1863.dist-info → prediction_market_agent_tooling-0.56.1.dist-info}/RECORD +20 -18
  18. prediction_market_agent_tooling/jobs/jobs.py +0 -45
  19. {prediction_market_agent_tooling-0.56.0.dev1863.dist-info → prediction_market_agent_tooling-0.56.1.dist-info}/LICENSE +0 -0
  20. {prediction_market_agent_tooling-0.56.0.dev1863.dist-info → prediction_market_agent_tooling-0.56.1.dist-info}/WHEEL +0 -0
  21. {prediction_market_agent_tooling-0.56.0.dev1863.dist-info → prediction_market_agent_tooling-0.56.1.dist-info}/entry_points.txt +0 -0
@@ -153,10 +153,10 @@ class DeployableAgent:
153
153
  input=input,
154
154
  output=output,
155
155
  user_id=user_id or getpass.getuser(),
156
- session_id=session_id
157
- or self.session_id, # All traces within a single run execution will be grouped under a single session.
158
- version=version
159
- or APIKeys().LANGFUSE_DEPLOYMENT_VERSION, # Optionally, mark the current deployment with version (e.g. add git commit hash during docker building).
156
+ session_id=session_id or self.session_id,
157
+ # All traces within a single run execution will be grouped under a single session.
158
+ version=version or APIKeys().LANGFUSE_DEPLOYMENT_VERSION,
159
+ # Optionally, mark the current deployment with version (e.g. add git commit hash during docker building).
160
160
  release=release,
161
161
  metadata=metadata,
162
162
  tags=tags,
@@ -342,6 +342,10 @@ class DeployablePredictionAgent(DeployableAgent):
342
342
  ]
343
343
  )
344
344
 
345
+ @property
346
+ def agent_name(self) -> str:
347
+ return self.__class__.__name__
348
+
345
349
  def check_min_required_balance_to_operate(self, market_type: MarketType) -> None:
346
350
  api_keys = APIKeys()
347
351
 
@@ -444,7 +448,9 @@ class DeployablePredictionAgent(DeployableAgent):
444
448
  ) -> None:
445
449
  keys = APIKeys()
446
450
  if self.store_prediction:
447
- market.store_prediction(processed_market=processed_market, keys=keys)
451
+ market.store_prediction(
452
+ processed_market=processed_market, keys=keys, agent_name=self.agent_name
453
+ )
448
454
  else:
449
455
  logger.info(
450
456
  f"Prediction {processed_market} not stored because {self.store_prediction=}."
@@ -519,7 +525,6 @@ class DeployableTraderAgent(DeployablePredictionAgent):
519
525
  def initialize_langfuse(self) -> None:
520
526
  super().initialize_langfuse()
521
527
  # Auto-observe all the methods where it makes sense, so that subclassses don't need to do it manually.
522
- self.get_betting_strategy = observe()(self.get_betting_strategy) # type: ignore[method-assign]
523
528
  self.build_trades = observe()(self.build_trades) # type: ignore[method-assign]
524
529
 
525
530
  def check_min_required_balance_to_trade(self, market: AgentMarket) -> None:
@@ -555,16 +560,18 @@ class DeployableTraderAgent(DeployablePredictionAgent):
555
560
  BettingStrategy.assert_trades_currency_match_markets(market, trades)
556
561
  return trades
557
562
 
563
+ def before_process_market(
564
+ self, market_type: MarketType, market: AgentMarket
565
+ ) -> None:
566
+ super().before_process_market(market_type, market)
567
+ self.check_min_required_balance_to_trade(market)
568
+
558
569
  def process_market(
559
570
  self,
560
571
  market_type: MarketType,
561
572
  market: AgentMarket,
562
573
  verify_market: bool = True,
563
574
  ) -> ProcessedTradedMarket | None:
564
- self.check_min_required_balance_to_trade(
565
- market
566
- ) # Do this as part of `process_market` because it observes some methods --> To keep everything traced under the `process_market`.
567
-
568
575
  processed_market = super().process_market(market_type, market, verify_market)
569
576
  if processed_market is None:
570
577
  return None
@@ -612,10 +619,14 @@ class DeployableTraderAgent(DeployablePredictionAgent):
612
619
  processed_market: ProcessedMarket | None,
613
620
  ) -> None:
614
621
  api_keys = APIKeys()
615
- super().after_process_market(market_type, market, processed_market)
622
+ super().after_process_market(
623
+ market_type,
624
+ market,
625
+ processed_market,
626
+ )
616
627
  if isinstance(processed_market, ProcessedTradedMarket):
617
628
  if self.store_trades:
618
- market.store_trades(processed_market, api_keys)
629
+ market.store_trades(processed_market, api_keys, self.agent_name)
619
630
  else:
620
631
  logger.info(
621
632
  f"Trades {processed_market.trades} not stored because {self.store_trades=}."
@@ -3,7 +3,12 @@ from abc import ABC, abstractmethod
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
6
- from prediction_market_agent_tooling.markets.agent_market import AgentMarket
6
+ from prediction_market_agent_tooling.deploy.betting_strategy import ProbabilisticAnswer
7
+ from prediction_market_agent_tooling.gtypes import Probability
8
+ from prediction_market_agent_tooling.markets.agent_market import (
9
+ AgentMarket,
10
+ ProcessedTradedMarket,
11
+ )
7
12
  from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
8
13
  FilterBy,
9
14
  SortBy,
@@ -39,10 +44,24 @@ class JobAgentMarket(AgentMarket, ABC):
39
44
  @classmethod
40
45
  @abstractmethod
41
46
  def get_jobs(
42
- cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy
47
+ cls,
48
+ limit: int | None,
49
+ filter_by: FilterBy = FilterBy.OPEN,
50
+ sort_by: SortBy = SortBy.CLOSING_SOONEST,
43
51
  ) -> t.Sequence["JobAgentMarket"]:
44
52
  """Get all available jobs."""
45
53
 
54
+ @staticmethod
55
+ @abstractmethod
56
+ def get_job(id: str) -> "JobAgentMarket":
57
+ """Get a single job by its id."""
58
+
59
+ @abstractmethod
60
+ def submit_job_result(
61
+ self, agent_name: str, max_bond: float, result: str
62
+ ) -> ProcessedTradedMarket:
63
+ """Submit the completed result for this job."""
64
+
46
65
  def to_simple_job(self, max_bond: float) -> SimpleJob:
47
66
  return SimpleJob(
48
67
  id=self.id,
@@ -51,3 +70,9 @@ class JobAgentMarket(AgentMarket, ABC):
51
70
  currency=self.currency.value,
52
71
  deadline=self.deadline,
53
72
  )
73
+
74
+ def get_job_answer(self, result: str) -> ProbabilisticAnswer:
75
+ # Just return 100% yes with 100% confidence, because we assume the job is completed correctly.
76
+ return ProbabilisticAnswer(
77
+ p_yes=Probability(1.0), confidence=1.0, reasoning=result
78
+ )
@@ -1,15 +1,14 @@
1
1
  import typing as t
2
2
 
3
- from web3 import Web3
4
-
3
+ from prediction_market_agent_tooling.config import APIKeys
5
4
  from prediction_market_agent_tooling.deploy.betting_strategy import (
6
5
  Currency,
7
6
  KellyBettingStrategy,
8
- ProbabilisticAnswer,
9
7
  TradeType,
10
8
  )
11
- from prediction_market_agent_tooling.gtypes import Probability
12
9
  from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
10
+ from prediction_market_agent_tooling.markets.agent_market import ProcessedTradedMarket
11
+ from prediction_market_agent_tooling.markets.data_models import PlacedTrade, Trade
13
12
  from prediction_market_agent_tooling.markets.omen.omen import (
14
13
  BetAmount,
15
14
  OmenAgentMarket,
@@ -36,11 +35,27 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
36
35
  return self.close_time
37
36
 
38
37
  def get_reward(self, max_bond: float) -> float:
39
- return compute_job_reward(self, max_bond)
38
+ trade = self.get_job_trade(
39
+ max_bond,
40
+ result="", # Pass empty result, as we are computing only potential reward at this point.
41
+ )
42
+ reward = (
43
+ self.get_buy_token_amount(
44
+ bet_amount=BetAmount(
45
+ amount=trade.amount.amount, currency=trade.amount.currency
46
+ ),
47
+ direction=trade.outcome,
48
+ ).amount
49
+ - trade.amount.amount
50
+ )
51
+ return reward
40
52
 
41
53
  @classmethod
42
54
  def get_jobs(
43
- cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy
55
+ cls,
56
+ limit: int | None,
57
+ filter_by: FilterBy = FilterBy.OPEN,
58
+ sort_by: SortBy = SortBy.CLOSING_SOONEST,
44
59
  ) -> t.Sequence["OmenJobAgentMarket"]:
45
60
  markets = OmenSubgraphHandler().get_omen_binary_markets_simple(
46
61
  limit=limit,
@@ -50,6 +65,52 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
50
65
  )
51
66
  return [OmenJobAgentMarket.from_omen_market(market) for market in markets]
52
67
 
68
+ @staticmethod
69
+ def get_job(id: str) -> "OmenJobAgentMarket":
70
+ return OmenJobAgentMarket.from_omen_agent_market(
71
+ OmenJobAgentMarket.get_binary_market(id=id)
72
+ )
73
+
74
+ def submit_job_result(
75
+ self, agent_name: str, max_bond: float, result: str
76
+ ) -> ProcessedTradedMarket:
77
+ if not APIKeys().enable_ipfs_upload:
78
+ raise RuntimeError(
79
+ f"ENABLE_IPFS_UPLOAD must be set to True to upload job results."
80
+ )
81
+
82
+ trade = self.get_job_trade(max_bond, result)
83
+ buy_id = self.buy_tokens(outcome=trade.outcome, amount=trade.amount)
84
+
85
+ processed_traded_market = ProcessedTradedMarket(
86
+ answer=self.get_job_answer(result),
87
+ trades=[PlacedTrade.from_trade(trade, id=buy_id)],
88
+ )
89
+
90
+ keys = APIKeys()
91
+ self.store_trades(processed_traded_market, keys, agent_name)
92
+
93
+ return processed_traded_market
94
+
95
+ def get_job_trade(self, max_bond: float, result: str) -> Trade:
96
+ # Because jobs are powered by prediction markets, potentional reward depends on job's liquidity and our will to bond (bet) our xDai into our job completion.
97
+ strategy = KellyBettingStrategy(max_bet_amount=max_bond)
98
+ required_trades = strategy.calculate_trades(
99
+ existing_position=None,
100
+ answer=self.get_job_answer(result),
101
+ market=self,
102
+ )
103
+ assert (
104
+ len(required_trades) == 1
105
+ ), f"Shouldn't process same job twice: {required_trades}"
106
+ trade = required_trades[0]
107
+ assert trade.trade_type == TradeType.BUY, "Should only buy on job markets."
108
+ assert trade.outcome, "Should buy only YES on job markets."
109
+ assert (
110
+ trade.amount.currency == Currency.xDai
111
+ ), "Should work only on real-money markets."
112
+ return required_trades[0]
113
+
53
114
  @staticmethod
54
115
  def from_omen_market(market: OmenMarket) -> "OmenJobAgentMarket":
55
116
  return OmenJobAgentMarket.from_omen_agent_market(
@@ -77,38 +138,3 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
77
138
  finalized_time=market.finalized_time,
78
139
  fees=market.fees,
79
140
  )
80
-
81
-
82
- def compute_job_reward(
83
- market: OmenAgentMarket, max_bond: float, web3: Web3 | None = None
84
- ) -> float:
85
- # Because jobs are powered by prediction markets, potentional reward depends on job's liquidity and our will to bond (bet) our xDai into our job completion.
86
- strategy = KellyBettingStrategy(max_bet_amount=max_bond)
87
- required_trades = strategy.calculate_trades(
88
- existing_position=None,
89
- # We assume that we finish the job and so the probability of the market happening will be 100%.
90
- answer=ProbabilisticAnswer(p_yes=Probability(1.0), confidence=1.0),
91
- market=market,
92
- )
93
-
94
- assert (
95
- len(required_trades) == 1
96
- ), f"Shouldn't process same job twice: {required_trades}"
97
- trade = required_trades[0]
98
- assert trade.trade_type == TradeType.BUY, "Should only buy on job markets."
99
- assert trade.outcome, "Should buy only YES on job markets."
100
- assert (
101
- trade.amount.currency == Currency.xDai
102
- ), "Should work only on real-money markets."
103
-
104
- reward = (
105
- market.get_buy_token_amount(
106
- bet_amount=BetAmount(
107
- amount=trade.amount.amount, currency=trade.amount.currency
108
- ),
109
- direction=trade.outcome,
110
- ).amount
111
- - trade.amount.amount
112
- )
113
-
114
- return reward
@@ -231,7 +231,10 @@ class AgentMarket(BaseModel):
231
231
  raise NotImplementedError("Subclasses must implement this method")
232
232
 
233
233
  def store_prediction(
234
- self, processed_market: ProcessedMarket | None, keys: APIKeys
234
+ self,
235
+ processed_market: ProcessedMarket | None,
236
+ keys: APIKeys,
237
+ agent_name: str,
235
238
  ) -> None:
236
239
  """
237
240
  If market allows to upload predictions somewhere, implement it in this method.
@@ -239,7 +242,10 @@ class AgentMarket(BaseModel):
239
242
  raise NotImplementedError("Subclasses must implement this method")
240
243
 
241
244
  def store_trades(
242
- self, traded_market: ProcessedTradedMarket | None, keys: APIKeys
245
+ self,
246
+ traded_market: ProcessedTradedMarket | None,
247
+ keys: APIKeys,
248
+ agent_name: str,
243
249
  ) -> None:
244
250
  """
245
251
  If market allows to upload trades somewhere, implement it in this method.
@@ -0,0 +1,51 @@
1
+ import typing as t
2
+
3
+ import tenacity
4
+ from pydantic import BaseModel
5
+ from subgrounds import FieldPath, Subgrounds
6
+
7
+ from prediction_market_agent_tooling.config import APIKeys
8
+ from prediction_market_agent_tooling.loggers import logger
9
+ from prediction_market_agent_tooling.tools.singleton import SingletonMeta
10
+
11
+ T = t.TypeVar("T", bound=BaseModel)
12
+
13
+
14
+ class BaseSubgraphHandler(metaclass=SingletonMeta):
15
+ def __init__(self) -> None:
16
+ self.sg = Subgrounds()
17
+ # Patch methods to retry on failure.
18
+ self.sg.query_json = tenacity.retry(
19
+ stop=tenacity.stop_after_attempt(3),
20
+ wait=tenacity.wait_fixed(1),
21
+ after=lambda x: logger.debug(f"query_json failed, {x.attempt_number=}."),
22
+ )(self.sg.query_json)
23
+ self.sg.load_subgraph = tenacity.retry(
24
+ stop=tenacity.stop_after_attempt(3),
25
+ wait=tenacity.wait_fixed(1),
26
+ after=lambda x: logger.debug(f"load_subgraph failed, {x.attempt_number=}."),
27
+ )(self.sg.load_subgraph)
28
+
29
+ self.keys = APIKeys()
30
+
31
+ def _parse_items_from_json(
32
+ self, result: list[dict[str, t.Any]]
33
+ ) -> list[dict[str, t.Any]]:
34
+ """subgrounds return a weird key as a dict key"""
35
+ items = []
36
+ for result_chunk in result:
37
+ for k, v in result_chunk.items():
38
+ # subgrounds might pack all items as a list, indexed by a key, or pack it as a dictionary (if one single element)
39
+ if v is None:
40
+ continue
41
+ elif isinstance(v, dict):
42
+ items.extend([v])
43
+ else:
44
+ items.extend(v)
45
+ return items
46
+
47
+ def do_query(self, fields: list[FieldPath], pydantic_model: t.Type[T]) -> list[T]:
48
+ result = self.sg.query_json(fields)
49
+ items = self._parse_items_from_json(result)
50
+ models = [pydantic_model.model_validate(i) for i in items]
51
+ return models
@@ -3,6 +3,8 @@ from datetime import timedelta
3
3
  from enum import Enum
4
4
 
5
5
  from prediction_market_agent_tooling.config import APIKeys
6
+ from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
7
+ from prediction_market_agent_tooling.jobs.omen.omen_jobs import OmenJobAgentMarket
6
8
  from prediction_market_agent_tooling.markets.agent_market import (
7
9
  AgentMarket,
8
10
  FilterBy,
@@ -46,6 +48,12 @@ class MarketType(str, Enum):
46
48
  raise ValueError(f"Unknown market type: {self}")
47
49
  return MARKET_TYPE_TO_AGENT_MARKET[self]
48
50
 
51
+ @property
52
+ def job_class(self) -> type[JobAgentMarket]:
53
+ if self not in JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET:
54
+ raise ValueError(f"Unknown market type: {self}")
55
+ return JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET[self]
56
+
49
57
  @property
50
58
  def is_blockchain_market(self) -> bool:
51
59
  return self in [MarketType.OMEN, MarketType.POLYMARKET]
@@ -58,6 +66,10 @@ MARKET_TYPE_TO_AGENT_MARKET: dict[MarketType, type[AgentMarket]] = {
58
66
  MarketType.METACULUS: MetaculusAgentMarket,
59
67
  }
60
68
 
69
+ JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET: dict[MarketType, type[JobAgentMarket]] = {
70
+ MarketType.OMEN: OmenJobAgentMarket,
71
+ }
72
+
61
73
 
62
74
  def get_binary_markets(
63
75
  limit: int,
@@ -107,7 +107,7 @@ class MetaculusAgentMarket(AgentMarket):
107
107
  return [MetaculusAgentMarket.from_data_model(q) for q in all_questions[:limit]]
108
108
 
109
109
  def store_prediction(
110
- self, processed_market: ProcessedMarket | None, keys: APIKeys
110
+ self, processed_market: ProcessedMarket | None, keys: APIKeys, agent_name: str
111
111
  ) -> None:
112
112
  if processed_market is not None:
113
113
  make_prediction(self.id, processed_market.answer.p_yes)
@@ -24,6 +24,7 @@ from prediction_market_agent_tooling.markets.data_models import (
24
24
  ResolvedBet,
25
25
  )
26
26
  from prediction_market_agent_tooling.tools.utils import (
27
+ BPS_CONSTANT,
27
28
  DatetimeUTC,
28
29
  check_not_none,
29
30
  should_not_happen,
@@ -502,7 +503,7 @@ class OmenBet(BaseModel):
502
503
  feeAmount: Wei
503
504
  outcomeIndex: int
504
505
  outcomeTokensTraded: Wei
505
- transactionHash: HexAddress
506
+ transactionHash: HexBytes
506
507
  fpmm: OmenMarket
507
508
 
508
509
  @property
@@ -785,6 +786,14 @@ class ContractPrediction(BaseModel):
785
786
  tx_hashes: list[HexBytes] = Field(..., alias="txHashes")
786
787
  estimated_probability_bps: int = Field(..., alias="estimatedProbabilityBps")
787
788
 
789
+ @property
790
+ def estimated_probability(self) -> Probability:
791
+ return Probability(self.estimated_probability_bps / BPS_CONSTANT)
792
+
793
+ @property
794
+ def boolean_outcome(self) -> bool:
795
+ return self.estimated_probability > 0.5
796
+
788
797
  @computed_field # type: ignore[prop-decorator] # Mypy issue: https://github.com/python/mypy/issues/14461
789
798
  @property
790
799
  def publisher_checksummed(self) -> ChecksumAddress:
@@ -798,7 +807,7 @@ class ContractPrediction(BaseModel):
798
807
 
799
808
  class IPFSAgentResult(BaseModel):
800
809
  reasoning: str
801
-
810
+ agent_name: str
802
811
  model_config = ConfigDict(
803
812
  extra="forbid",
804
813
  )
@@ -78,6 +78,7 @@ from prediction_market_agent_tooling.tools.contract import (
78
78
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
79
79
  from prediction_market_agent_tooling.tools.ipfs.ipfs_handler import IPFSHandler
80
80
  from prediction_market_agent_tooling.tools.utils import (
81
+ BPS_CONSTANT,
81
82
  DatetimeUTC,
82
83
  calculate_sell_amount_in_collateral,
83
84
  check_not_none,
@@ -413,17 +414,21 @@ class OmenAgentMarket(AgentMarket):
413
414
  @staticmethod
414
415
  def verify_operational_balance(api_keys: APIKeys) -> bool:
415
416
  return get_total_balance(
416
- api_keys.public_key, # Use `public_key`, not `bet_from_address` because transaction costs are paid from the EOA wallet.
417
+ api_keys.public_key,
418
+ # Use `public_key`, not `bet_from_address` because transaction costs are paid from the EOA wallet.
417
419
  sum_wxdai=False,
418
420
  ) > xdai_type(0.001)
419
421
 
420
422
  def store_prediction(
421
- self, processed_market: ProcessedMarket | None, keys: APIKeys
423
+ self, processed_market: ProcessedMarket | None, keys: APIKeys, agent_name: str
422
424
  ) -> None:
423
425
  """On Omen, we have to store predictions along with trades, see `store_trades`."""
424
426
 
425
427
  def store_trades(
426
- self, traded_market: ProcessedTradedMarket | None, keys: APIKeys
428
+ self,
429
+ traded_market: ProcessedTradedMarket | None,
430
+ keys: APIKeys,
431
+ agent_name: str,
427
432
  ) -> None:
428
433
  if traded_market is None:
429
434
  logger.warning(f"No prediction for market {self.id}, not storing anything.")
@@ -437,7 +442,7 @@ class OmenAgentMarket(AgentMarket):
437
442
  if keys.enable_ipfs_upload:
438
443
  logger.info("Storing prediction on IPFS.")
439
444
  ipfs_hash = IPFSHandler(keys).store_agent_result(
440
- IPFSAgentResult(reasoning=reasoning)
445
+ IPFSAgentResult(reasoning=reasoning, agent_name=agent_name)
441
446
  )
442
447
  ipfs_hash_decoded = ipfscidv0_to_byte32(ipfs_hash)
443
448
 
@@ -448,7 +453,7 @@ class OmenAgentMarket(AgentMarket):
448
453
  publisher=keys.public_key,
449
454
  ipfs_hash=ipfs_hash_decoded,
450
455
  tx_hashes=tx_hashes,
451
- estimated_probability_bps=int(traded_market.answer.p_yes * 10000),
456
+ estimated_probability_bps=int(traded_market.answer.p_yes * BPS_CONSTANT),
452
457
  )
453
458
  tx_receipt = OmenAgentResultMappingContract().add_prediction(
454
459
  api_keys=keys,
@@ -1237,12 +1242,12 @@ def redeem_from_all_user_positions(
1237
1242
 
1238
1243
  if not conditional_token_contract.is_condition_resolved(condition_id):
1239
1244
  logger.info(
1240
- f"[{index+1} / {len(user_positions)}] Skipping redeem, {user_position.id=} isn't resolved yet."
1245
+ f"[{index + 1} / {len(user_positions)}] Skipping redeem, {user_position.id=} isn't resolved yet."
1241
1246
  )
1242
1247
  continue
1243
1248
 
1244
1249
  logger.info(
1245
- f"[{index+1} / {len(user_positions)}] Processing redeem from {user_position.id=}."
1250
+ f"[{index + 1} / {len(user_positions)}] Processing redeem from {user_position.id=}."
1246
1251
  )
1247
1252
 
1248
1253
  original_balances = get_balances(public_key, web3)
@@ -1263,9 +1268,11 @@ def redeem_from_all_user_positions(
1263
1268
  def get_binary_market_p_yes_history(market: OmenAgentMarket) -> list[Probability]:
1264
1269
  history: list[Probability] = []
1265
1270
  trades = sorted(
1266
- OmenSubgraphHandler().get_trades( # We need to look at price both after buying or selling, so get trades, not bets.
1271
+ OmenSubgraphHandler().get_trades(
1272
+ # We need to look at price both after buying or selling, so get trades, not bets.
1267
1273
  market_id=market.market_maker_contract_address_checksummed,
1268
- end_time=market.close_time, # Even after market is closed, there can be many `Sell` trades which will converge the probability to the true one.
1274
+ end_time=market.close_time,
1275
+ # Even after market is closed, there can be many `Sell` trades which will converge the probability to the true one.
1269
1276
  ),
1270
1277
  key=lambda x: x.creation_datetime,
1271
1278
  )
@@ -2,12 +2,10 @@ import sys
2
2
  import typing as t
3
3
 
4
4
  import requests
5
- import tenacity
6
5
  from PIL import Image
7
6
  from PIL.Image import Image as ImageType
8
- from subgrounds import FieldPath, Subgrounds
7
+ from subgrounds import FieldPath
9
8
 
10
- from prediction_market_agent_tooling.config import APIKeys
11
9
  from prediction_market_agent_tooling.gtypes import (
12
10
  ChecksumAddress,
13
11
  HexAddress,
@@ -15,8 +13,10 @@ from prediction_market_agent_tooling.gtypes import (
15
13
  Wei,
16
14
  wei_type,
17
15
  )
18
- from prediction_market_agent_tooling.loggers import logger
19
16
  from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
17
+ from prediction_market_agent_tooling.markets.base_subgraph_handler import (
18
+ BaseSubgraphHandler,
19
+ )
20
20
  from prediction_market_agent_tooling.markets.omen.data_models import (
21
21
  OMEN_BINARY_MARKET_OUTCOMES,
22
22
  ContractPrediction,
@@ -33,7 +33,6 @@ from prediction_market_agent_tooling.markets.omen.omen_contracts import (
33
33
  WrappedxDaiContract,
34
34
  sDaiContract,
35
35
  )
36
- from prediction_market_agent_tooling.tools.singleton import SingletonMeta
37
36
  from prediction_market_agent_tooling.tools.utils import (
38
37
  DatetimeUTC,
39
38
  to_int_timestamp,
@@ -51,7 +50,7 @@ SAFE_COLLATERAL_TOKEN_MARKETS = (
51
50
  )
52
51
 
53
52
 
54
- class OmenSubgraphHandler(metaclass=SingletonMeta):
53
+ class OmenSubgraphHandler(BaseSubgraphHandler):
55
54
  """
56
55
  Class responsible for handling interactions with Omen subgraphs (trades, conditionalTokens).
57
56
  """
@@ -64,52 +63,38 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
64
63
 
65
64
  OMEN_IMAGE_MAPPING_GRAPH_URL = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/EWN14ciGK53PpUiKSm7kMWQ6G4iz3tDrRLyZ1iXMQEdu"
66
65
 
67
- OMEN_AGENT_RESULT_MAPPING_GRAPH_URL = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/GoE3UFyc8Gg9xzv92oinonyhRCphpGu62qB2Eh2XvJ8F"
66
+ OMEN_AGENT_RESULT_MAPPING_GRAPH_URL = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/J6bJEnbqJpAvNyQE8i58M9mKF4zqo33BEJRdnXmqa6Kn"
68
67
 
69
68
  INVALID_ANSWER = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
70
69
 
71
70
  def __init__(self) -> None:
72
- self.sg = Subgrounds()
73
-
74
- # Patch methods to retry on failure.
75
- self.sg.query_json = tenacity.retry(
76
- stop=tenacity.stop_after_attempt(3),
77
- wait=tenacity.wait_fixed(1),
78
- after=lambda x: logger.debug(f"query_json failed, {x.attempt_number=}."),
79
- )(self.sg.query_json)
80
- self.sg.load_subgraph = tenacity.retry(
81
- stop=tenacity.stop_after_attempt(3),
82
- wait=tenacity.wait_fixed(1),
83
- after=lambda x: logger.debug(f"load_subgraph failed, {x.attempt_number=}."),
84
- )(self.sg.load_subgraph)
85
-
86
- keys = APIKeys()
71
+ super().__init__()
87
72
 
88
73
  # Load the subgraph
89
74
  self.trades_subgraph = self.sg.load_subgraph(
90
75
  self.OMEN_TRADES_SUBGRAPH.format(
91
- graph_api_key=keys.graph_api_key.get_secret_value()
76
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
92
77
  )
93
78
  )
94
79
  self.conditional_tokens_subgraph = self.sg.load_subgraph(
95
80
  self.CONDITIONAL_TOKENS_SUBGRAPH.format(
96
- graph_api_key=keys.graph_api_key.get_secret_value()
81
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
97
82
  )
98
83
  )
99
84
  self.realityeth_subgraph = self.sg.load_subgraph(
100
85
  self.REALITYETH_GRAPH_URL.format(
101
- graph_api_key=keys.graph_api_key.get_secret_value()
86
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
102
87
  )
103
88
  )
104
89
  self.omen_image_mapping_subgraph = self.sg.load_subgraph(
105
90
  self.OMEN_IMAGE_MAPPING_GRAPH_URL.format(
106
- graph_api_key=keys.graph_api_key.get_secret_value()
91
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
107
92
  )
108
93
  )
109
94
 
110
95
  self.omen_agent_result_mapping_subgraph = self.sg.load_subgraph(
111
96
  self.OMEN_AGENT_RESULT_MAPPING_GRAPH_URL.format(
112
- graph_api_key=keys.graph_api_key.get_secret_value()
97
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
113
98
  )
114
99
  )
115
100
 
@@ -446,14 +431,8 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
446
431
  **optional_params,
447
432
  )
448
433
 
449
- omen_markets = self.do_markets_query(markets)
450
- return omen_markets
451
-
452
- def do_markets_query(self, markets: FieldPath) -> list[OmenMarket]:
453
434
  fields = self._get_fields_for_markets(markets)
454
- result = self.sg.query_json(fields)
455
- items = self._parse_items_from_json(result)
456
- omen_markets = [OmenMarket.model_validate(i) for i in items]
435
+ omen_markets = self.do_query(fields=fields, pydantic_model=OmenMarket)
457
436
  return omen_markets
458
437
 
459
438
  def get_omen_market_by_market_id(self, market_id: HexAddress) -> OmenMarket:
@@ -461,7 +440,8 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
461
440
  id=market_id.lower()
462
441
  )
463
442
 
464
- omen_markets = self.do_markets_query(markets)
443
+ fields = self._get_fields_for_markets(markets)
444
+ omen_markets = self.do_query(fields=fields, pydantic_model=OmenMarket)
465
445
 
466
446
  if len(omen_markets) != 1:
467
447
  raise ValueError(
@@ -470,22 +450,6 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
470
450
 
471
451
  return omen_markets[0]
472
452
 
473
- def _parse_items_from_json(
474
- self, result: list[dict[str, t.Any]]
475
- ) -> list[dict[str, t.Any]]:
476
- """subgrounds return a weird key as a dict key"""
477
- items = []
478
- for result_chunk in result:
479
- for k, v in result_chunk.items():
480
- # subgrounds might pack all items as a list, indexed by a key, or pack it as a dictionary (if one single element)
481
- if v is None:
482
- continue
483
- elif isinstance(v, dict):
484
- items.extend([v])
485
- else:
486
- items.extend(v)
487
- return items
488
-
489
453
  def _get_fields_for_user_positions(
490
454
  self, user_positions: FieldPath
491
455
  ) -> list[FieldPath]:
@@ -944,3 +908,17 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
944
908
  if not items:
945
909
  return []
946
910
  return [ContractPrediction.model_validate(i) for i in items]
911
+
912
+ def get_agent_results_for_bet(self, bet: OmenBet) -> ContractPrediction | None:
913
+ results = [
914
+ result
915
+ for result in self.get_agent_results_for_market(bet.fpmm.id)
916
+ if bet.transactionHash in result.tx_hashes
917
+ ]
918
+
919
+ if not results:
920
+ return None
921
+ elif len(results) > 1:
922
+ raise RuntimeError("Multiple results found for a single bet.")
923
+
924
+ return results[0]
@@ -0,0 +1,27 @@
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+ from prediction_market_agent_tooling.gtypes import HexBytes
4
+
5
+
6
+ class SeerMarket(BaseModel):
7
+ model_config = ConfigDict(populate_by_name=True)
8
+
9
+ id: HexBytes
10
+ title: str = Field(alias="marketName")
11
+ outcomes: list[str]
12
+ parent_market: HexBytes = Field(alias="parentMarket")
13
+ wrapped_tokens: list[HexBytes] = Field(alias="wrappedTokens")
14
+
15
+
16
+ class SeerToken(BaseModel):
17
+ id: HexBytes
18
+ name: str
19
+ symbol: str
20
+
21
+
22
+ class SeerPool(BaseModel):
23
+ model_config = ConfigDict(populate_by_name=True)
24
+ id: HexBytes
25
+ liquidity: int
26
+ token0: SeerToken
27
+ token1: SeerToken
@@ -0,0 +1,142 @@
1
+ from typing import Any
2
+
3
+ from subgrounds import FieldPath
4
+ from web3.constants import ADDRESS_ZERO
5
+
6
+ from prediction_market_agent_tooling.markets.base_subgraph_handler import (
7
+ BaseSubgraphHandler,
8
+ )
9
+ from prediction_market_agent_tooling.markets.seer.data_models import (
10
+ SeerMarket,
11
+ SeerPool,
12
+ )
13
+ from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
14
+
15
+ INVALID_OUTCOME = "Invalid result"
16
+
17
+
18
+ class SeerSubgraphHandler(BaseSubgraphHandler):
19
+ """
20
+ Class responsible for handling interactions with Seer subgraphs.
21
+ """
22
+
23
+ SEER_SUBGRAPH = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/B4vyRqJaSHD8dRDb3BFRoAzuBK18c1QQcXq94JbxDxWH"
24
+
25
+ SWAPR_ALGEBRA_SUBGRAPH = "https://gateway-arbitrum.network.thegraph.com/api/{graph_api_key}/subgraphs/id/AAA1vYjxwFHzbt6qKwLHNcDSASyr1J1xVViDH8gTMFMR"
26
+
27
+ INVALID_ANSWER = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
28
+
29
+ def __init__(self) -> None:
30
+ super().__init__()
31
+
32
+ self.seer_subgraph = self.sg.load_subgraph(
33
+ self.SEER_SUBGRAPH.format(
34
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
35
+ )
36
+ )
37
+ self.swapr_algebra_subgraph = self.sg.load_subgraph(
38
+ self.SWAPR_ALGEBRA_SUBGRAPH.format(
39
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
40
+ )
41
+ )
42
+
43
+ def _get_fields_for_markets(self, markets_field: FieldPath) -> list[FieldPath]:
44
+ fields = [
45
+ markets_field.id,
46
+ markets_field.factory,
47
+ markets_field.creator,
48
+ markets_field.marketName,
49
+ markets_field.outcomes,
50
+ markets_field.parentMarket,
51
+ markets_field.finalizeTs,
52
+ markets_field.wrappedTokens,
53
+ ]
54
+ return fields
55
+
56
+ @staticmethod
57
+ def filter_bicategorical_markets(markets: list[SeerMarket]) -> list[SeerMarket]:
58
+ # We do an extra check for the invalid outcome for safety.
59
+ return [
60
+ m for m in markets if len(m.outcomes) == 3 and INVALID_OUTCOME in m.outcomes
61
+ ]
62
+
63
+ @staticmethod
64
+ def filter_binary_markets(markets: list[SeerMarket]) -> list[SeerMarket]:
65
+ return [
66
+ market
67
+ for market in markets
68
+ if {"yes", "no"}.issubset({o.lower() for o in market.outcomes})
69
+ ]
70
+
71
+ @staticmethod
72
+ def build_filter_for_conditional_markets(
73
+ include_conditional_markets: bool = True,
74
+ ) -> dict[Any, Any]:
75
+ return (
76
+ {}
77
+ if include_conditional_markets
78
+ else {"parentMarket": ADDRESS_ZERO.lower()}
79
+ )
80
+
81
+ def get_bicategorical_markets(
82
+ self, include_conditional_markets: bool = True
83
+ ) -> list[SeerMarket]:
84
+ """Returns markets that contain 2 categories plus an invalid outcome."""
85
+ # Binary markets on Seer contain 3 outcomes: OutcomeA, outcomeB and an Invalid option.
86
+ query_filter = self.build_filter_for_conditional_markets(
87
+ include_conditional_markets
88
+ )
89
+ query_filter["outcomes_contains"] = [INVALID_OUTCOME]
90
+ markets_field = self.seer_subgraph.Query.markets(where=query_filter)
91
+ fields = self._get_fields_for_markets(markets_field)
92
+ markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
93
+ two_category_markets = self.filter_bicategorical_markets(markets)
94
+ return two_category_markets
95
+
96
+ def get_binary_markets(
97
+ self, include_conditional_markets: bool = True
98
+ ) -> list[SeerMarket]:
99
+ two_category_markets = self.get_bicategorical_markets(
100
+ include_conditional_markets=include_conditional_markets
101
+ )
102
+ # Now we additionally filter markets based on YES/NO being the only outcomes.
103
+ binary_markets = self.filter_binary_markets(two_category_markets)
104
+ return binary_markets
105
+
106
+ def get_market_by_id(self, market_id: HexBytes) -> SeerMarket:
107
+ markets_field = self.seer_subgraph.Query.market(id=market_id.hex().lower())
108
+ fields = self._get_fields_for_markets(markets_field)
109
+ markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
110
+ if len(markets) != 1:
111
+ raise ValueError(
112
+ f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
113
+ )
114
+ return markets[0]
115
+
116
+ def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]:
117
+ fields = [
118
+ pools_field.id,
119
+ pools_field.liquidity,
120
+ pools_field.token0.id,
121
+ pools_field.token0.name,
122
+ pools_field.token0.symbol,
123
+ pools_field.token1.id,
124
+ pools_field.token1.name,
125
+ pools_field.token1.symbol,
126
+ ]
127
+ return fields
128
+
129
+ def get_pools_for_market(self, market: SeerMarket) -> list[SeerPool]:
130
+ # We iterate through the wrapped tokens and put them in a where clause so that we hit the subgraph endpoint just once.
131
+ wheres = []
132
+ for wrapped_token in market.wrapped_tokens:
133
+ wheres.extend(
134
+ [
135
+ {"token0": wrapped_token.hex().lower()},
136
+ {"token1": wrapped_token.hex().lower()},
137
+ ]
138
+ )
139
+ pools_field = self.swapr_algebra_subgraph.Query.pools(where={"or": wheres})
140
+ fields = self._get_fields_for_pools(pools_field)
141
+ pools = self.do_query(fields=fields, pydantic_model=SeerPool)
142
+ return pools
@@ -91,40 +91,29 @@ def db_cache(
91
91
 
92
92
  api_keys = api_keys if api_keys is not None else APIKeys()
93
93
 
94
- sqlalchemy_db_url = api_keys.SQLALCHEMY_DB_URL
95
- if sqlalchemy_db_url is None:
96
- logger.warning(
97
- f"SQLALCHEMY_DB_URL not provided in the environment, skipping function caching."
98
- )
94
+ @wraps(func)
95
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
96
+ # If caching is disabled, just call the function and return it
97
+ if not api_keys.ENABLE_CACHE:
98
+ return func(*args, **kwargs)
99
99
 
100
- engine = (
101
- create_engine(
102
- sqlalchemy_db_url.get_secret_value(),
100
+ engine = create_engine(
101
+ api_keys.sqlalchemy_db_url.get_secret_value(),
103
102
  # Use custom json serializer and deserializer, because otherwise, for example `datetime` serialization would fail.
104
103
  json_serializer=json_serializer,
105
104
  json_deserializer=json_deserializer,
106
105
  )
107
- if sqlalchemy_db_url is not None
108
- else None
109
- )
110
106
 
111
- # Create table if it doesn't exist
112
- if engine is not None:
107
+ # Create table if it doesn't exist
113
108
  SQLModel.metadata.create_all(engine)
114
109
 
115
- @wraps(func)
116
- def wrapper(*args: Any, **kwargs: Any) -> Any:
117
- # If caching is disabled, just call the function and return it
118
- if not api_keys.ENABLE_CACHE:
119
- return func(*args, **kwargs)
120
-
121
110
  # Convert *args and **kwargs to a single dictionary, where we have names for arguments passed as args as well.
122
111
  signature = inspect.signature(func)
123
112
  bound_arguments = signature.bind(*args, **kwargs)
124
113
  bound_arguments.apply_defaults()
125
114
 
126
115
  # Convert any argument that is Pydantic model into classic dictionary, otherwise it won't be json-serializable.
127
- args_dict = convert_pydantic_to_dict(bound_arguments.arguments)
116
+ args_dict: dict[str, Any] = bound_arguments.arguments
128
117
 
129
118
  # Remove `self` or `cls` if present (in case of class' methods)
130
119
  if "self" in args_dict:
@@ -162,25 +151,21 @@ def db_cache(
162
151
  if return_type is not None and contains_pydantic_model(return_type):
163
152
  is_pydantic_model = True
164
153
 
165
- # If postgres access was specified, try to find a hit
166
- if engine is not None:
167
- with Session(engine) as session:
168
- # Try to get cached result
169
- statement = (
170
- select(FunctionCache)
171
- .where(
172
- FunctionCache.function_name == function_name,
173
- FunctionCache.full_function_name == full_function_name,
174
- FunctionCache.args_hash == args_hash,
175
- )
176
- .order_by(desc(FunctionCache.created_at))
154
+ with Session(engine) as session:
155
+ # Try to get cached result
156
+ statement = (
157
+ select(FunctionCache)
158
+ .where(
159
+ FunctionCache.function_name == function_name,
160
+ FunctionCache.full_function_name == full_function_name,
161
+ FunctionCache.args_hash == args_hash,
177
162
  )
178
- if max_age is not None:
179
- cutoff_time = utcnow() - max_age
180
- statement = statement.where(FunctionCache.created_at >= cutoff_time)
181
- cached_result = session.exec(statement).first()
182
- else:
183
- cached_result = None
163
+ .order_by(desc(FunctionCache.created_at))
164
+ )
165
+ if max_age is not None:
166
+ cutoff_time = utcnow() - max_age
167
+ statement = statement.where(FunctionCache.created_at >= cutoff_time)
168
+ cached_result = session.exec(statement).first()
184
169
 
185
170
  if cached_result:
186
171
  logger.info(
@@ -211,18 +196,12 @@ def db_cache(
211
196
 
212
197
  # If postgres access was specified, save it.
213
198
  if engine is not None and (cache_none or computed_result is not None):
214
- # In case of Pydantic outputs, we need to dictionarize it for it to be serializable.
215
- result_data = (
216
- convert_pydantic_to_dict(computed_result)
217
- if is_pydantic_model
218
- else computed_result
219
- )
220
199
  cache_entry = FunctionCache(
221
200
  function_name=function_name,
222
201
  full_function_name=full_function_name,
223
202
  args_hash=args_hash,
224
203
  args=args_dict,
225
- result=result_data,
204
+ result=computed_result,
226
205
  created_at=utcnow(),
227
206
  )
228
207
  with Session(engine) as session:
@@ -249,9 +228,12 @@ def contains_pydantic_model(return_type: Any) -> bool:
249
228
  return False
250
229
 
251
230
 
252
- def json_serializer_default_fn(y: DatetimeUTC | timedelta | date) -> str:
231
+ def json_serializer_default_fn(
232
+ y: DatetimeUTC | timedelta | date | BaseModel,
233
+ ) -> str | dict[str, Any]:
253
234
  """
254
235
  Used to serialize objects that don't support it by default into a specific string that can be deserialized out later.
236
+ If this function returns a dictionary, it will be called recursivelly.
255
237
  If you add something here, also add it to `replace_custom_stringified_objects` below.
256
238
  """
257
239
  if isinstance(y, DatetimeUTC):
@@ -260,6 +242,8 @@ def json_serializer_default_fn(y: DatetimeUTC | timedelta | date) -> str:
260
242
  return f"timedelta::{y.total_seconds()}"
261
243
  elif isinstance(y, date):
262
244
  return f"date::{y.isoformat()}"
245
+ elif isinstance(y, BaseModel):
246
+ return y.model_dump()
263
247
  raise TypeError(
264
248
  f"Unsuported type for the default json serialize function, value is {y}."
265
249
  )
@@ -298,22 +282,6 @@ def json_deserializer(s: str) -> Any:
298
282
  return replace_custom_stringified_objects(data)
299
283
 
300
284
 
301
- def convert_pydantic_to_dict(value: Any) -> Any:
302
- """
303
- Convert Pydantic models to dictionaries, including if they are in nested structures.
304
- """
305
- if isinstance(value, BaseModel):
306
- return value.model_dump()
307
- elif isinstance(value, dict):
308
- return {k: convert_pydantic_to_dict(v) for k, v in value.items()}
309
- elif isinstance(value, (list, tuple)):
310
- return type(value)(convert_pydantic_to_dict(v) for v in value)
311
- elif isinstance(value, set):
312
- return {convert_pydantic_to_dict(v) for v in value}
313
- else:
314
- return value
315
-
316
-
317
285
  def convert_cached_output_to_pydantic(return_type: Any, data: Any) -> Any:
318
286
  """
319
287
  Used to initialize Pydantic models from anything cached that was originally a Pydantic model in the output. Including models in nested structures.
@@ -34,7 +34,7 @@ QUESTION_IS_INVALID_PROMPT = """Main signs about an invalid question (sometimes
34
34
  - Which could give an incentive only to specific participants to commit an immoral violent action, but are in practice unlikely.
35
35
  - Valid: Will the US be engaged in a military conflict with a UN member state in 2021? (It’s unlikely for the US to declare war in order to win a bet on this market).
36
36
  - Valid: Will Derek Chauvin go to jail for the murder of George Flyod? (It’s unlikely that the jurors would collude to make a wrong verdict in order to win this market).
37
- - Questions with relative dates will resolve as invalid. Dates must be stated in absolute terms, not relative depending on the current time. But they can be relative to the even specified in the question itself.
37
+ - Questions with relative dates will resolve as invalid. Dates must be stated in absolute terms, not relative depending on the current time. But they can be relative to the event specified in the question itself.
38
38
  - Invalid: Who will be the president of the United States in 6 months? ("in 6 months depends on the current time").
39
39
  - Invalid: In the next 14 days, will Gnosis Chain gain another 1M users? ("in the next 14 days depends on the current time").
40
40
  - Valid: Will GNO price go up 10 days after Gnosis Pay cashback program is annouced? ("10 days after" is relative to the event in the question, so we can determine absolute value).
@@ -28,6 +28,8 @@ LLM_SUPER_LOW_TEMPERATURE = 0.00000001
28
28
  # For consistent results, also include seed for models that supports it.
29
29
  LLM_SEED = 0
30
30
 
31
+ BPS_CONSTANT = 10000
32
+
31
33
 
32
34
  def check_not_none(
33
35
  value: Optional[T],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prediction-market-agent-tooling
3
- Version: 0.56.0.dev1863
3
+ Version: 0.56.1
4
4
  Summary: Tools to benchmark, deploy and monitor prediction market agents.
5
5
  Author: Gnosis
6
6
  Requires-Python: >=3.10,<3.12
@@ -17,7 +17,7 @@ prediction_market_agent_tooling/benchmark/agents.py,sha256=B1-uWdyeN4GGKMWGK_-Cc
17
17
  prediction_market_agent_tooling/benchmark/benchmark.py,sha256=MqTiaaJ3cYiOLUVR7OyImLWxcEya3Rl5JyFYW-K0lwM,17097
18
18
  prediction_market_agent_tooling/benchmark/utils.py,sha256=D0MfUkVZllmvcU0VOurk9tcKT7JTtwwOp-63zuCBVuc,2880
19
19
  prediction_market_agent_tooling/config.py,sha256=114f3V9abaok27p5jX3UVr5b5gRUiSxBIYn8Snid34I,6731
20
- prediction_market_agent_tooling/deploy/agent.py,sha256=v6jnk8gY_ckF_Z8Slj408RLBadC5cZwx_nqV02TiThU,22642
20
+ prediction_market_agent_tooling/deploy/agent.py,sha256=dpc94DUo8Gq1LdRdw6k78vm_47OeJIfomG9CRVpgzk0,22757
21
21
  prediction_market_agent_tooling/deploy/agent_example.py,sha256=dIIdZashExWk9tOdyDjw87AuUcGyM7jYxNChYrVK2dM,1001
22
22
  prediction_market_agent_tooling/deploy/betting_strategy.py,sha256=kMrIE3wMv_IB6nJd_1DmDXDkEZhsXFOgyTd7JZ0gqHI,13068
23
23
  prediction_market_agent_tooling/deploy/constants.py,sha256=M5ty8URipYMGe_G-RzxRydK3AFL6CyvmqCraJUrLBnE,82
@@ -27,11 +27,11 @@ prediction_market_agent_tooling/deploy/gcp/utils.py,sha256=oyW0jgrUT2Tr49c7GlpcM
27
27
  prediction_market_agent_tooling/deploy/trade_interval.py,sha256=Xk9j45alQ_vrasGvsNyuW70XHIQ7wfvjoxNR3F6HYCw,1155
28
28
  prediction_market_agent_tooling/gtypes.py,sha256=tqp03PyY0Yhievl4XELfwAn0xOoecaTvBZ1Co6b-A7o,2541
29
29
  prediction_market_agent_tooling/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- prediction_market_agent_tooling/jobs/jobs.py,sha256=I07yh0GJ-xhlvQaOUQB8xlSnihhcbU2c7DZ4ZND14c0,1246
31
- prediction_market_agent_tooling/jobs/jobs_models.py,sha256=I5uBTHJ2S1Wi3H4jDxxU7nsswSIP9r3BevHmljLh5Pg,1370
32
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py,sha256=I2_vGrEJj1reSI8M377ab5QCsYNp_l4l4QeYEmDBkFM,3989
30
+ prediction_market_agent_tooling/jobs/jobs_models.py,sha256=GOtsNm7URhzZM5fPY64r8m8Gz-sSsUhG1qmDoC7wGL8,2231
31
+ prediction_market_agent_tooling/jobs/omen/omen_jobs.py,sha256=N0_jGDyXQeVXXlYg4oA_pOfqIjscHsLQbr0pBwFGoRo,5178
33
32
  prediction_market_agent_tooling/loggers.py,sha256=Am6HHXRNO545BO3l7Ue9Wb2TkYE1OK8KKhGbI3XypVU,3751
34
- prediction_market_agent_tooling/markets/agent_market.py,sha256=OgB6bvDGfTAxbh6cDGD3XFO0iy0MAaOQvXEP6nw8xW8,12817
33
+ prediction_market_agent_tooling/markets/agent_market.py,sha256=W2ME57-CSAhrt8qm8-b5r7yLq-Sk7R_BZMaApvjhrUE,12901
34
+ prediction_market_agent_tooling/markets/base_subgraph_handler.py,sha256=IxDTwX4tej9j5olNkXcLIE0RCF1Nh2icZQUT2ERMmZo,1937
35
35
  prediction_market_agent_tooling/markets/categorize.py,sha256=jsoHWvZk9pU6n17oWSCcCxNNYVwlb_NXsZxKRI7vmsk,1301
36
36
  prediction_market_agent_tooling/markets/data_models.py,sha256=jMqrSFO_w2z-5N3PFVgZqTHdVdkzSDhhzky2lHsGGKA,3621
37
37
  prediction_market_agent_tooling/markets/manifold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -40,21 +40,23 @@ prediction_market_agent_tooling/markets/manifold/data_models.py,sha256=ylXIEHymx
40
40
  prediction_market_agent_tooling/markets/manifold/manifold.py,sha256=qemQIwuFg4yf6egGWFp9lWpz1lXr02QiBeZ2akcT6II,5026
41
41
  prediction_market_agent_tooling/markets/manifold/utils.py,sha256=cPPFWXm3vCYH1jy7_ctJZuQH9ZDaPL4_AgAYzGWkoow,513
42
42
  prediction_market_agent_tooling/markets/market_fees.py,sha256=Q64T9uaJx0Vllt0BkrPmpMEz53ra-hMVY8Czi7CEP7s,1227
43
- prediction_market_agent_tooling/markets/markets.py,sha256=mwubc567OIlA32YKqlIdTloYV8FGJia9gPv0wE0xUEA,3368
43
+ prediction_market_agent_tooling/markets/markets.py,sha256=_b-BAfoKIcXl5ZXVODi1ywMhRCbc52022csH1nQT084,3893
44
44
  prediction_market_agent_tooling/markets/metaculus/api.py,sha256=4TRPGytQQbSdf42DCg2M_JWYPAuNjqZ3eBqaQBLkNks,2736
45
45
  prediction_market_agent_tooling/markets/metaculus/data_models.py,sha256=Suxa7xELdYuFNKqvGvFh8qyfVtAg79E-vaQ6dqNZOtA,3261
46
- prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=E_TUf5q73lWzdMp40Ne-3w4MjEd7AHcaif4pvFh9FMU,4360
46
+ prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=86TIx6cavEWc8Cv4KpZxSvwiSw9oFybXE3YB49pg-CA,4377
47
47
  prediction_market_agent_tooling/markets/omen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
- prediction_market_agent_tooling/markets/omen/data_models.py,sha256=nCjsc-ylIzQOCK_1BW-5NoYrS-NIXz2Hg9N1-IqhhC8,27516
49
- prediction_market_agent_tooling/markets/omen/omen.py,sha256=LqNZjngo6LoktKecfYmGmJJ9D5rj-s0Poy4x4_GZfp0,51116
48
+ prediction_market_agent_tooling/markets/omen/data_models.py,sha256=0eky-RO0TuJysUXHd52A6DSU2mx1ZWiJvvntS4xQsUc,27794
49
+ prediction_market_agent_tooling/markets/omen/omen.py,sha256=uOuV2DgQmxz6kzPcMovyGg0xYS0c6x12fEFNtLmN-uY,51260
50
50
  prediction_market_agent_tooling/markets/omen/omen_contracts.py,sha256=Zq7SncCq-hvpgXKsVruGBGCn1OhKZTe7r1qLdCTrT2w,28297
51
51
  prediction_market_agent_tooling/markets/omen/omen_resolving.py,sha256=iDWdjICGkt968exwCjY-6nsnQyrrNAg3YjnDdP430GQ,9415
52
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py,sha256=zQH3iu0SVH1RmE-W3NMEpcKVMILXJYxMhL6w1wh5RUo,37348
52
+ prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py,sha256=cXgtBfc5uv7d8598SJ537LxFGVfF_mv-VoQoqI4_G84,36330
53
53
  prediction_market_agent_tooling/markets/polymarket/api.py,sha256=UZ4_TG8ceb9Y-qgsOKs8Qiv8zDt957QkT8IX2c83yqo,4800
54
54
  prediction_market_agent_tooling/markets/polymarket/data_models.py,sha256=Fd5PI5y3mJM8VHExBhWFWEnuuIKxQmIAXgBuoPDvNjw,4341
55
55
  prediction_market_agent_tooling/markets/polymarket/data_models_web.py,sha256=VZhVccTApygSKMmy6Au2G02JCJOKJnR_oVeKlaesuSg,12548
56
56
  prediction_market_agent_tooling/markets/polymarket/polymarket.py,sha256=NRoZK71PtH8kkangMqme7twcAXhRJSSabbmOir-UnAI,3418
57
57
  prediction_market_agent_tooling/markets/polymarket/utils.py,sha256=m4JG6WULh5epCJt4XBMHg0ae5NoVhqlOvAl0A7DR9iM,2023
58
+ prediction_market_agent_tooling/markets/seer/data_models.py,sha256=2f6DOXhGerYbTSk5vUvw_y87TcUxOtSfca8-Et7imtU,643
59
+ prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py,sha256=Jola8AxmNDljBCzl6Gvjb9qH75Y7D5dAwbdZl_jndN8,5478
58
60
  prediction_market_agent_tooling/monitor/markets/manifold.py,sha256=TS4ERwTfQnot8dhekNyVNhJYf5ysYsjF-9v5_kM3aVI,3334
59
61
  prediction_market_agent_tooling/monitor/markets/metaculus.py,sha256=LOnyWWBFdg10-cTWdb76nOsNjDloO8OfMT85GBzRCFI,1455
60
62
  prediction_market_agent_tooling/monitor/markets/omen.py,sha256=EqiJYTvDbSu7fBpbrBmCuf3fc6GHr4MxWrBGa69MIyc,3305
@@ -69,7 +71,7 @@ prediction_market_agent_tooling/tools/betting_strategies/market_moving.py,sha256
69
71
  prediction_market_agent_tooling/tools/betting_strategies/minimum_bet_to_win.py,sha256=-FUSuQQgjcWSSnoFxnlAyTeilY6raJABJVM2QKkFqAY,438
70
72
  prediction_market_agent_tooling/tools/betting_strategies/stretch_bet_between.py,sha256=THMXwFlskvzbjnX_OiYtDSzI8XVFyULWfP2525_9UGc,429
71
73
  prediction_market_agent_tooling/tools/betting_strategies/utils.py,sha256=kpIb-ci67Vc1Yqqaa-_S4OUkbhWSIYog4_Iwp69HU_k,97
72
- prediction_market_agent_tooling/tools/caches/db_cache.py,sha256=iHd_Qi4hMlzUIwT700i196A9ScaQCptKQPH1taQRGEQ,13859
74
+ prediction_market_agent_tooling/tools/caches/db_cache.py,sha256=KOhwgd4wDiNifJBb3iXRMlxxp7j9l-eSFZvgQ5_Y7xg,12687
73
75
  prediction_market_agent_tooling/tools/caches/inmemory_cache.py,sha256=tGHHd9HCiE_hCCtPtloHZQdDfBuiow9YsqJNYi2Tx_0,499
74
76
  prediction_market_agent_tooling/tools/contract.py,sha256=s3yo8IbXTcvAJcPfLM0_NbgaEsWwLsPmyVnOgyjq_xI,20919
75
77
  prediction_market_agent_tooling/tools/costs.py,sha256=EaAJ7v9laD4VEV3d8B44M4u3_oEO_H16jRVCdoZ93Uw,954
@@ -81,7 +83,7 @@ prediction_market_agent_tooling/tools/httpx_cached_client.py,sha256=0-N1r0zcGKlY
81
83
  prediction_market_agent_tooling/tools/image_gen/image_gen.py,sha256=HzRwBx62hOXBOmrtpkXaP9Qq1Ku03uUGdREocyjLQ_k,1266
82
84
  prediction_market_agent_tooling/tools/image_gen/market_thumbnail_gen.py,sha256=8A3U2uxsCsOfLjru-6R_PPIAuiKY4qFkWp_GSBPV6-s,1280
83
85
  prediction_market_agent_tooling/tools/ipfs/ipfs_handler.py,sha256=CTTMfTvs_8PH4kAtlQby2aeEKwgpmxtuGbd4oYIdJ2A,1201
84
- prediction_market_agent_tooling/tools/is_invalid.py,sha256=Gp8ASGmqXZGdKKQ4U2ZfQj6bx2Wngb4IExIfV54E4j4,5322
86
+ prediction_market_agent_tooling/tools/is_invalid.py,sha256=GSMwSWUZy-xviaFoIl0L34AVfLLTdh7zegjsTFE7_1M,5323
85
87
  prediction_market_agent_tooling/tools/is_predictable.py,sha256=VGkxSoJ8CSLknloOLzm5J4-us7XImYxVzvpsAzxbpCc,6730
86
88
  prediction_market_agent_tooling/tools/langfuse_.py,sha256=jI_4ROxqo41CCnWGS1vN_AeDVhRzLMaQLxH3kxDu3L8,1153
87
89
  prediction_market_agent_tooling/tools/langfuse_client_utils.py,sha256=B0PhAQyviFnVbtOCYMxYmcCn66cu9nbqAOIAZcdgiRI,5771
@@ -95,10 +97,10 @@ prediction_market_agent_tooling/tools/singleton.py,sha256=CiIELUiI-OeS7U7eeHEt0r
95
97
  prediction_market_agent_tooling/tools/streamlit_user_login.py,sha256=NXEqfjT9Lc9QtliwSGRASIz1opjQ7Btme43H4qJbzgE,3010
96
98
  prediction_market_agent_tooling/tools/tavily/tavily_models.py,sha256=5ldQs1pZe6uJ5eDAuP4OLpzmcqYShlIV67kttNFvGS0,342
97
99
  prediction_market_agent_tooling/tools/tavily/tavily_search.py,sha256=Kw2mXNkMTYTEe1MBSTqhQmLoeXtgb6CkmHlcAJvhtqE,3809
98
- prediction_market_agent_tooling/tools/utils.py,sha256=W-9SqeCKd51BYMRhDjYPQ7lfNO_zE9EvYpmu2r5WXGA,7163
100
+ prediction_market_agent_tooling/tools/utils.py,sha256=1VvunbTmzGzpIlRukFhArreFNxJPbsg4lLtQNk0r2bY,7185
99
101
  prediction_market_agent_tooling/tools/web3_utils.py,sha256=44W8siSLNQxeib98bbwAe7V5C609NHNlUuxwuWIRDiY,11838
100
- prediction_market_agent_tooling-0.56.0.dev1863.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
101
- prediction_market_agent_tooling-0.56.0.dev1863.dist-info/METADATA,sha256=6sEM3cB9ZgytNkbgnHz2Y-IlHy86yV08HnWlf7B82nE,8114
102
- prediction_market_agent_tooling-0.56.0.dev1863.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
103
- prediction_market_agent_tooling-0.56.0.dev1863.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
104
- prediction_market_agent_tooling-0.56.0.dev1863.dist-info/RECORD,,
102
+ prediction_market_agent_tooling-0.56.1.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
103
+ prediction_market_agent_tooling-0.56.1.dist-info/METADATA,sha256=fGK0gWUcUB0hphd3qfYiurnr3dl549L6qQiYKgCpczM,8106
104
+ prediction_market_agent_tooling-0.56.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
105
+ prediction_market_agent_tooling-0.56.1.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
106
+ prediction_market_agent_tooling-0.56.1.dist-info/RECORD,,
@@ -1,45 +0,0 @@
1
- import typing as t
2
-
3
- from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
4
- from prediction_market_agent_tooling.jobs.omen.omen_jobs import OmenJobAgentMarket
5
- from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
6
- from prediction_market_agent_tooling.markets.markets import MarketType
7
-
8
- JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET: dict[MarketType, type[JobAgentMarket]] = {
9
- MarketType.OMEN: OmenJobAgentMarket,
10
- }
11
-
12
-
13
- @t.overload
14
- def get_jobs(
15
- market_type: t.Literal[MarketType.OMEN],
16
- limit: int | None,
17
- filter_by: FilterBy = FilterBy.OPEN,
18
- sort_by: SortBy = SortBy.NONE,
19
- ) -> t.Sequence[OmenJobAgentMarket]:
20
- ...
21
-
22
-
23
- @t.overload
24
- def get_jobs(
25
- market_type: MarketType,
26
- limit: int | None,
27
- filter_by: FilterBy = FilterBy.OPEN,
28
- sort_by: SortBy = SortBy.NONE,
29
- ) -> t.Sequence[JobAgentMarket]:
30
- ...
31
-
32
-
33
- def get_jobs(
34
- market_type: MarketType,
35
- limit: int | None,
36
- filter_by: FilterBy = FilterBy.OPEN,
37
- sort_by: SortBy = SortBy.NONE,
38
- ) -> t.Sequence[JobAgentMarket]:
39
- job_class = JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET[market_type]
40
- markets = job_class.get_jobs(
41
- limit=limit,
42
- sort_by=sort_by,
43
- filter_by=filter_by,
44
- )
45
- return markets