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
@@ -0,0 +1,28 @@
1
+ import httpx
2
+
3
+ from prediction_market_agent_tooling.gtypes import ChainID, ChecksumAddress
4
+ from prediction_market_agent_tooling.markets.seer.data_models import SeerTransaction
5
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
6
+ from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
7
+
8
+
9
+ def get_seer_transactions(
10
+ account: ChecksumAddress,
11
+ chain_id: ChainID,
12
+ start_time: DatetimeUTC | None = None,
13
+ end_time: DatetimeUTC | None = None,
14
+ timeout: int = 60, # The endpoint is pretty slow to respond atm.
15
+ ) -> list[SeerTransaction]:
16
+ url = "https://app.seer.pm/.netlify/functions/get-transactions"
17
+ params: dict[str, str | int] = {
18
+ "account": account,
19
+ "chainId": chain_id,
20
+ "startTime": to_int_timestamp(start_time) if start_time else 0,
21
+ "endTime": to_int_timestamp(end_time if end_time else utcnow()),
22
+ }
23
+ response = httpx.get(url, params=params, timeout=timeout)
24
+ response.raise_for_status()
25
+ response_json = response.json()
26
+
27
+ transactions = [SeerTransaction.model_validate(tx) for tx in response_json]
28
+ return transactions
@@ -8,14 +8,20 @@ from prediction_market_agent_tooling.gtypes import (
8
8
  ABI,
9
9
  ChecksumAddress,
10
10
  OutcomeStr,
11
+ OutcomeWei,
11
12
  TxReceipt,
13
+ Wei,
12
14
  xDai,
13
15
  )
14
- from prediction_market_agent_tooling.markets.seer.data_models import RedeemParams
16
+ from prediction_market_agent_tooling.markets.seer.data_models import (
17
+ ExactInputSingleParams,
18
+ QuoteExactInputSingleParams,
19
+ )
15
20
  from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
16
21
  CreateCategoricalMarketsParams,
17
22
  )
18
23
  from prediction_market_agent_tooling.tools.contract import (
24
+ ContractERC20OnGnosisChain,
19
25
  ContractOnGnosisChain,
20
26
  abi_field_validator,
21
27
  )
@@ -83,7 +89,7 @@ class SeerMarketFactory(ContractOnGnosisChain):
83
89
 
84
90
 
85
91
  class GnosisRouter(ContractOnGnosisChain):
86
- # https://gnosisscan.io/address/0x83183da839ce8228e31ae41222ead9edbb5cdcf1#code.
92
+ # https://gnosisscan.io/address/0xeC9048b59b3467415b1a38F63416407eA0c70fB8#code.
87
93
  abi: ABI = abi_field_validator(
88
94
  os.path.join(
89
95
  os.path.dirname(os.path.realpath(__file__)),
@@ -97,12 +103,18 @@ class GnosisRouter(ContractOnGnosisChain):
97
103
  def redeem_to_base(
98
104
  self,
99
105
  api_keys: APIKeys,
100
- params: RedeemParams,
106
+ market: ChecksumAddress,
107
+ outcome_indexes: list[int],
108
+ amounts: list[OutcomeWei],
101
109
  web3: Web3 | None = None,
102
110
  ) -> TxReceipt:
103
- params_dict = params.model_dump(by_alias=True)
104
111
  # We explicity set amounts since OutcomeWei gets serialized as dict
105
- params_dict["amounts"] = [amount.value for amount in params.amounts]
112
+ params_dict = {
113
+ "market": market,
114
+ "outcomeIndexes": outcome_indexes,
115
+ "amounts": [amount.value for amount in amounts],
116
+ }
117
+
106
118
  receipt_tx = self.send(
107
119
  api_keys=api_keys,
108
120
  function_name="redeemToBase",
@@ -110,3 +122,101 @@ class GnosisRouter(ContractOnGnosisChain):
110
122
  web3=web3,
111
123
  )
112
124
  return receipt_tx
125
+
126
+ def split_from_base(
127
+ self,
128
+ api_keys: APIKeys,
129
+ market_id: ChecksumAddress,
130
+ amount_wei: Wei,
131
+ web3: Web3 | None = None,
132
+ ) -> TxReceipt:
133
+ """Splits using xDAI and receives outcome tokens"""
134
+ return self.send_with_value(
135
+ api_keys=api_keys,
136
+ function_name="splitFromBase",
137
+ amount_wei=amount_wei,
138
+ function_params=[market_id],
139
+ web3=web3,
140
+ )
141
+
142
+ def split_position(
143
+ self,
144
+ api_keys: APIKeys,
145
+ collateral_token: ChecksumAddress,
146
+ market_id: ChecksumAddress,
147
+ amount: Wei,
148
+ web3: Web3 | None = None,
149
+ ) -> TxReceipt:
150
+ """Splits collateral token into full set of outcome tokens."""
151
+ receipt_tx = self.send(
152
+ api_keys=api_keys,
153
+ function_name="splitPosition",
154
+ function_params=[collateral_token, market_id, amount],
155
+ web3=web3,
156
+ )
157
+ return receipt_tx
158
+
159
+
160
+ class SwaprRouterContract(ContractOnGnosisChain):
161
+ abi: ABI = abi_field_validator(
162
+ os.path.join(
163
+ os.path.dirname(os.path.realpath(__file__)),
164
+ "../../abis/swapr_router.abi.json",
165
+ )
166
+ )
167
+
168
+ address: ChecksumAddress = Web3.to_checksum_address(
169
+ "0xffb643e73f280b97809a8b41f7232ab401a04ee1"
170
+ )
171
+
172
+ def exact_input_single(
173
+ self,
174
+ api_keys: APIKeys,
175
+ params: ExactInputSingleParams,
176
+ web3: Web3 | None = None,
177
+ ) -> TxReceipt:
178
+ erc20_token = ContractERC20OnGnosisChain(address=params.token_in)
179
+
180
+ if (
181
+ erc20_token.allowance(api_keys.bet_from_address, self.address, web3=web3)
182
+ < params.amount_in
183
+ ):
184
+ erc20_token.approve(api_keys, self.address, params.amount_in, web3=web3)
185
+
186
+ return self.send(
187
+ api_keys=api_keys,
188
+ function_name="exactInputSingle",
189
+ function_params=[tuple(dict(params).values())],
190
+ web3=web3,
191
+ # Use higher gas limit for complex swap operations to avoid slow estimation
192
+ # Typical Swapr swaps use 150k-300k gas, we set conservative
193
+ default_gas=400_000,
194
+ )
195
+
196
+
197
+ class SwaprQuoterContract(ContractOnGnosisChain):
198
+ # File content taken from https://gnosisscan.io/address/0xcBaD9FDf0D2814659Eb26f600EFDeAF005Eda0F7#code.
199
+ abi: ABI = abi_field_validator(
200
+ os.path.join(
201
+ os.path.dirname(os.path.realpath(__file__)),
202
+ "../../abis/swapr_quoter.abi.json",
203
+ )
204
+ )
205
+
206
+ address: ChecksumAddress = Web3.to_checksum_address(
207
+ "0xcBaD9FDf0D2814659Eb26f600EFDeAF005Eda0F7"
208
+ )
209
+
210
+ def quote_exact_input_single(
211
+ self,
212
+ params: QuoteExactInputSingleParams,
213
+ web3: Web3 | None = None,
214
+ ) -> tuple[Wei, Wei]:
215
+ # See https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps.
216
+ result: tuple[int, int] = self.call(
217
+ function_name="quoteExactInputSingle",
218
+ function_params=list(dict(params).values()),
219
+ web3=web3,
220
+ )
221
+ amount_out, fee = result
222
+ return Wei(amount_out), Wei(fee)
@@ -1,27 +1,56 @@
1
1
  import sys
2
2
  import typing as t
3
+ from collections import defaultdict
4
+ from enum import Enum
3
5
  from typing import Any
4
6
 
5
7
  from subgrounds import FieldPath
6
8
  from web3.constants import ADDRESS_ZERO
7
9
 
8
10
  from prediction_market_agent_tooling.deploy.constants import (
11
+ DOWN_OUTCOME_LOWERCASE_IDENTIFIER,
9
12
  NO_OUTCOME_LOWERCASE_IDENTIFIER,
13
+ UP_OUTCOME_LOWERCASE_IDENTIFIER,
10
14
  YES_OUTCOME_LOWERCASE_IDENTIFIER,
11
15
  )
12
16
  from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei
13
17
  from prediction_market_agent_tooling.loggers import logger
14
- from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
18
+ from prediction_market_agent_tooling.markets.agent_market import (
19
+ ConditionalFilterType,
20
+ FilterBy,
21
+ QuestionType,
22
+ SortBy,
23
+ )
15
24
  from prediction_market_agent_tooling.markets.base_subgraph_handler import (
16
25
  BaseSubgraphHandler,
17
26
  )
18
- from prediction_market_agent_tooling.markets.seer.data_models import SeerMarket
19
- from prediction_market_agent_tooling.markets.seer.subgraph_data_models import SeerPool
27
+ from prediction_market_agent_tooling.markets.seer.data_models import (
28
+ SeerMarket,
29
+ SeerMarketQuestions,
30
+ SeerMarketWithQuestions,
31
+ )
32
+ from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
33
+ SwaprPool,
34
+ SwaprSwap,
35
+ )
20
36
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
21
- from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
37
+ from prediction_market_agent_tooling.tools.singleton import SingletonMeta
38
+ from prediction_market_agent_tooling.tools.utils import (
39
+ DatetimeUTC,
40
+ to_int_timestamp,
41
+ utcnow,
42
+ )
22
43
  from prediction_market_agent_tooling.tools.web3_utils import unwrap_generic_value
23
44
 
24
45
 
46
+ class TemplateId(int, Enum):
47
+ """Template IDs used in Reality.eth questions."""
48
+
49
+ SCALAR = 1
50
+ CATEGORICAL = 2
51
+ MULTICATEGORICAL = 3
52
+
53
+
25
54
  class SeerSubgraphHandler(BaseSubgraphHandler):
26
55
  """
27
56
  Class responsible for handling interactions with Seer subgraphs.
@@ -47,7 +76,9 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
47
76
  )
48
77
  )
49
78
 
50
- def _get_fields_for_markets(self, markets_field: FieldPath) -> list[FieldPath]:
79
+ def _get_fields_for_markets(
80
+ self, markets_field: FieldPath, current_level: int = 0, max_level: int = 1
81
+ ) -> list[FieldPath]:
51
82
  fields = [
52
83
  markets_field.id,
53
84
  markets_field.factory,
@@ -61,12 +92,26 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
61
92
  markets_field.payoutNumerators,
62
93
  markets_field.hasAnswers,
63
94
  markets_field.blockTimestamp,
64
- markets_field.parentMarket.id,
65
95
  markets_field.openingTs,
66
96
  markets_field.finalizeTs,
67
97
  markets_field.wrappedTokens,
68
98
  markets_field.collateralToken,
99
+ markets_field.upperBound,
100
+ markets_field.lowerBound,
101
+ markets_field.templateId,
69
102
  ]
103
+ if current_level < max_level:
104
+ fields.extend(
105
+ self._get_fields_for_markets(
106
+ markets_field.parentMarket, current_level + 1, max_level
107
+ )
108
+ )
109
+ # TODO: Same situation as with `questions` field above.
110
+ # fields.extend(
111
+ # self._get_fields_for_markets(
112
+ # markets_field.childMarkets, current_level + 1, max_level
113
+ # )
114
+ # )
70
115
  return fields
71
116
 
72
117
  @staticmethod
@@ -74,12 +119,30 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
74
119
  # We do an extra check for the invalid outcome for safety.
75
120
  return [m for m in markets if len(m.outcomes) == 3]
76
121
 
122
+ @staticmethod
123
+ def _create_case_variations_condition(
124
+ identifier: str,
125
+ outcome_condition: str = "outcomes_contains",
126
+ condition: str = "or",
127
+ ) -> dict[str, list[dict[str, list[str]]]]:
128
+ return {
129
+ condition: [
130
+ {outcome_condition: [variation]}
131
+ for variation in [
132
+ identifier.lower(),
133
+ identifier.capitalize(),
134
+ identifier.upper(),
135
+ ]
136
+ ]
137
+ }
138
+
77
139
  @staticmethod
78
140
  def _build_where_statements(
79
141
  filter_by: FilterBy,
80
142
  outcome_supply_gt_if_open: Wei,
81
- include_conditional_markets: bool = False,
82
- include_categorical_markets: bool = True,
143
+ question_type: QuestionType = QuestionType.ALL,
144
+ conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
145
+ parent_market_id: HexBytes | None = None,
83
146
  ) -> dict[Any, Any]:
84
147
  now = to_int_timestamp(utcnow())
85
148
 
@@ -98,31 +161,62 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
98
161
  case _:
99
162
  raise ValueError(f"Unknown filter {filter_by}")
100
163
 
101
- if not include_conditional_markets:
102
- and_stms["parentMarket"] = ADDRESS_ZERO.lower()
164
+ if parent_market_id:
165
+ and_stms["parentMarket"] = parent_market_id.to_0x_hex().lower()
103
166
 
104
- # We are only interested in binary markets of type YES/NO/Invalid.
105
- yes_stms, no_stms = {}, {}
106
- if not include_categorical_markets:
107
- # Create single OR conditions with all variations
108
- yes_stms["or"] = [
109
- {"outcomes_contains": [variation]}
110
- for variation in [
111
- YES_OUTCOME_LOWERCASE_IDENTIFIER,
112
- YES_OUTCOME_LOWERCASE_IDENTIFIER.capitalize(),
113
- YES_OUTCOME_LOWERCASE_IDENTIFIER.upper(),
114
- ]
115
- ]
116
- no_stms["or"] = [
117
- {"outcomes_contains": [variation]}
118
- for variation in [
119
- NO_OUTCOME_LOWERCASE_IDENTIFIER,
120
- NO_OUTCOME_LOWERCASE_IDENTIFIER.capitalize(),
121
- NO_OUTCOME_LOWERCASE_IDENTIFIER.upper(),
122
- ]
123
- ]
167
+ outcome_filters: list[dict[str, t.Any]] = []
124
168
 
125
- where_stms: dict[str, t.Any] = {"and": [and_stms, yes_stms, no_stms]}
169
+ if question_type == QuestionType.SCALAR:
170
+ # Template ID "1" + UP/DOWN outcomes for scalar markets
171
+ and_stms["templateId"] = TemplateId.SCALAR.value
172
+ up_filter = SeerSubgraphHandler._create_case_variations_condition(
173
+ UP_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
174
+ )
175
+ down_filter = SeerSubgraphHandler._create_case_variations_condition(
176
+ DOWN_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
177
+ )
178
+ outcome_filters.extend([up_filter, down_filter])
179
+
180
+ elif question_type == QuestionType.BINARY:
181
+ # Template ID "2" + YES/NO outcomes for binary markets
182
+ and_stms["templateId"] = TemplateId.CATEGORICAL.value
183
+ yes_filter = SeerSubgraphHandler._create_case_variations_condition(
184
+ YES_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
185
+ )
186
+ no_filter = SeerSubgraphHandler._create_case_variations_condition(
187
+ NO_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
188
+ )
189
+ outcome_filters.extend([yes_filter, no_filter])
190
+
191
+ elif question_type == QuestionType.CATEGORICAL:
192
+ # Template ID 2 (categorical) OR Template ID 3 (multi-categorical,
193
+ # we treat them as categorical for now for simplicity)
194
+ # https://reality.eth.limo/app/docs/html/contracts.html#templates
195
+ outcome_filters.append(
196
+ {
197
+ "or": [
198
+ {"templateId": TemplateId.CATEGORICAL.value},
199
+ {"templateId": TemplateId.MULTICATEGORICAL.value},
200
+ ]
201
+ }
202
+ )
203
+
204
+ # Build filters for conditional_filter type
205
+ conditional_filter = {}
206
+ match conditional_filter_type:
207
+ case ConditionalFilterType.ONLY_CONDITIONAL:
208
+ conditional_filter["parentMarket_not"] = ADDRESS_ZERO.lower()
209
+ case ConditionalFilterType.ONLY_NOT_CONDITIONAL:
210
+ conditional_filter["parentMarket"] = ADDRESS_ZERO.lower()
211
+ case ConditionalFilterType.ALL:
212
+ pass
213
+ case _:
214
+ raise ValueError(
215
+ f"Unknown conditional filter {conditional_filter_type}"
216
+ )
217
+
218
+ all_filters = outcome_filters + [and_stms, conditional_filter]
219
+ where_stms: dict[str, t.Any] = {"and": all_filters}
126
220
  return where_stms
127
221
 
128
222
  def _build_sort_params(
@@ -157,18 +251,18 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
157
251
  sort_by: SortBy = SortBy.NONE,
158
252
  limit: int | None = None,
159
253
  outcome_supply_gt_if_open: Wei = Wei(0),
160
- include_conditional_markets: bool = True,
161
- include_categorical_markets: bool = True,
162
- ) -> list[SeerMarket]:
254
+ question_type: QuestionType = QuestionType.ALL,
255
+ conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
256
+ parent_market_id: HexBytes | None = None,
257
+ ) -> list[SeerMarketWithQuestions]:
163
258
  sort_direction, sort_by_field = self._build_sort_params(sort_by)
164
259
 
165
- """Returns markets that contain 2 categories plus an invalid outcome."""
166
- # Binary markets on Seer contain 3 outcomes: OutcomeA, outcomeB and an Invalid option.
167
260
  where_stms = self._build_where_statements(
168
261
  filter_by=filter_by,
169
262
  outcome_supply_gt_if_open=outcome_supply_gt_if_open,
170
- include_conditional_markets=include_conditional_markets,
171
- include_categorical_markets=include_categorical_markets,
263
+ parent_market_id=parent_market_id,
264
+ question_type=question_type,
265
+ conditional_filter_type=conditional_filter_type,
172
266
  )
173
267
 
174
268
  # These values can not be set to `None`, but they can be omitted.
@@ -187,62 +281,123 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
187
281
  )
188
282
  fields = self._get_fields_for_markets(markets_field)
189
283
  markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
190
- return markets
284
+ market_ids = [m.id for m in markets]
285
+ # We fetch questions from all markets and all parents in one go
286
+ parent_market_ids = [
287
+ m.parent_market.id for m in markets if m.parent_market is not None
288
+ ]
289
+ q = SeerQuestionsCache(seer_subgraph_handler=self)
290
+ q.fetch_questions(list(set(market_ids + parent_market_ids)))
291
+
292
+ # Create SeerMarketWithQuestions for each market
293
+ return [
294
+ SeerMarketWithQuestions(
295
+ **m.model_dump(), questions=q.market_id_to_questions[m.id]
296
+ )
297
+ for m in markets
298
+ ]
299
+
300
+ def get_questions_for_markets(
301
+ self, market_ids: list[HexBytes]
302
+ ) -> list[SeerMarketQuestions]:
303
+ where = unwrap_generic_value(
304
+ {"market_in": [market_id.to_0x_hex().lower() for market_id in market_ids]}
305
+ )
306
+ markets_field = self.seer_subgraph.Query.marketQuestions(where=where)
307
+ fields = self._get_fields_for_questions(markets_field)
308
+ questions = self.do_query(fields=fields, pydantic_model=SeerMarketQuestions)
309
+ return questions
191
310
 
192
- def get_market_by_id(self, market_id: HexBytes) -> SeerMarket:
193
- markets_field = self.seer_subgraph.Query.market(id=market_id.hex().lower())
311
+ def get_market_by_id(self, market_id: HexBytes) -> SeerMarketWithQuestions:
312
+ markets_field = self.seer_subgraph.Query.market(
313
+ id=market_id.to_0x_hex().lower()
314
+ )
194
315
  fields = self._get_fields_for_markets(markets_field)
195
316
  markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
196
317
  if len(markets) != 1:
197
318
  raise ValueError(
198
319
  f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
199
320
  )
200
- return markets[0]
321
+ q = SeerQuestionsCache(self)
322
+ q.fetch_questions([market_id])
323
+ questions = q.market_id_to_questions[market_id]
324
+ s = SeerMarketWithQuestions.model_validate(
325
+ markets[0].model_dump() | {"questions": questions}
326
+ )
327
+ return s
201
328
 
202
- def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]:
329
+ def _get_fields_for_questions(self, questions_field: FieldPath) -> list[FieldPath]:
203
330
  fields = [
204
- pools_field.id,
205
- pools_field.liquidity,
206
- pools_field.sqrtPrice,
207
- pools_field.token0Price,
208
- pools_field.token1Price,
209
- pools_field.token0.id,
210
- pools_field.token0.name,
211
- pools_field.token0.symbol,
212
- pools_field.token1.id,
213
- pools_field.token1.name,
214
- pools_field.token1.symbol,
331
+ questions_field.question.id,
332
+ questions_field.question.best_answer,
333
+ questions_field.question.finalize_ts,
334
+ questions_field.market.id,
215
335
  ]
216
336
  return fields
217
337
 
338
+ def get_market_by_wrapped_token(self, tokens: list[ChecksumAddress]) -> SeerMarket:
339
+ where_stms = {"wrappedTokens_contains": tokens}
340
+ markets_field = self.seer_subgraph.Query.markets(
341
+ where=unwrap_generic_value(where_stms)
342
+ )
343
+ fields = self._get_fields_for_markets(markets_field)
344
+ markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
345
+ if len(markets) != 1:
346
+ raise ValueError(
347
+ f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
348
+ )
349
+ return markets[0]
350
+
351
+ def _get_fields_for_seer_token(self, fields: FieldPath) -> list[FieldPath]:
352
+ return [
353
+ fields.id,
354
+ fields.name,
355
+ fields.symbol,
356
+ ]
357
+
358
+ def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]:
359
+ fields = (
360
+ [
361
+ pools_field.id,
362
+ pools_field.liquidity,
363
+ pools_field.sqrtPrice,
364
+ pools_field.token0Price,
365
+ pools_field.token1Price,
366
+ pools_field.totalValueLockedToken0,
367
+ pools_field.totalValueLockedToken1,
368
+ ]
369
+ + self._get_fields_for_seer_token(pools_field.token0)
370
+ + self._get_fields_for_seer_token(pools_field.token1)
371
+ )
372
+ return fields
373
+
218
374
  def get_pool_by_token(
219
375
  self, token_address: ChecksumAddress, collateral_address: ChecksumAddress
220
- ) -> SeerPool | None:
376
+ ) -> SwaprPool | None:
221
377
  # We iterate through the wrapped tokens and put them in a where clause so that we hit the subgraph endpoint just once.
222
- wheres = []
223
- wheres.extend(
224
- [
378
+
379
+ where_argument = {
380
+ "or": [
225
381
  {
226
- "token0": token_address.lower(),
227
- "token1": collateral_address.lower(),
382
+ "token0_": {"id": token_address.lower()},
383
+ "token1_": {"id": collateral_address.lower()},
228
384
  },
229
385
  {
230
- "token0": collateral_address.lower(),
231
- "token1": token_address.lower(),
386
+ "token0_": {"id": collateral_address.lower()},
387
+ "token1_": {"id": token_address.lower()},
232
388
  },
233
389
  ]
234
- )
235
-
390
+ }
236
391
  optional_params = {}
237
392
  optional_params["orderBy"] = self.swapr_algebra_subgraph.Pool.liquidity
238
393
  optional_params["orderDirection"] = "desc"
239
394
 
240
395
  pools_field = self.swapr_algebra_subgraph.Query.pools(
241
- where=unwrap_generic_value({"or": wheres}), **optional_params
396
+ where=unwrap_generic_value(where_argument), **optional_params
242
397
  )
243
398
 
244
399
  fields = self._get_fields_for_pools(pools_field)
245
- pools = self.do_query(fields=fields, pydantic_model=SeerPool)
400
+ pools = self.do_query(fields=fields, pydantic_model=SwaprPool)
246
401
  # We assume there is only one pool for outcomeToken/sDAI.
247
402
  if len(pools) > 1:
248
403
  logger.info(
@@ -252,3 +407,79 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
252
407
  # We select the first one
253
408
  return pools[0]
254
409
  return None
410
+
411
+ def _get_fields_for_swaps(self, swaps_field: FieldPath) -> list[FieldPath]:
412
+ fields = (
413
+ [
414
+ swaps_field.id,
415
+ swaps_field.pool.id,
416
+ swaps_field.sender,
417
+ swaps_field.recipient,
418
+ swaps_field.price,
419
+ swaps_field.amount0,
420
+ swaps_field.amount1,
421
+ swaps_field.timestamp,
422
+ ]
423
+ + self._get_fields_for_seer_token(swaps_field.token0)
424
+ + self._get_fields_for_seer_token(swaps_field.token1)
425
+ )
426
+ return fields
427
+
428
+ def get_swaps(
429
+ self,
430
+ recipient: ChecksumAddress,
431
+ timestamp_gt: DatetimeUTC | None = None,
432
+ timestamp_lt: DatetimeUTC | None = None,
433
+ ) -> list[SwaprSwap]:
434
+ where_argument: dict[str, Any] = {"recipient": recipient.lower()}
435
+ if timestamp_gt is not None:
436
+ where_argument["timestamp_gt"] = to_int_timestamp(timestamp_gt)
437
+ if timestamp_lt is not None:
438
+ where_argument["timestamp_lt"] = to_int_timestamp(timestamp_lt)
439
+
440
+ swaps_field = self.swapr_algebra_subgraph.Query.swaps(where=where_argument)
441
+ fields = self._get_fields_for_swaps(swaps_field)
442
+ swaps = self.do_query(fields=fields, pydantic_model=SwaprSwap)
443
+
444
+ return swaps
445
+
446
+
447
+ class SeerQuestionsCache(metaclass=SingletonMeta):
448
+ """A singleton cache for storing and retrieving Seer market questions.
449
+
450
+ This class provides an in-memory cache for Seer market questions, preventing
451
+ redundant subgraph queries by maintaining a mapping of market IDs to their
452
+ associated questions. It implements the singleton pattern to ensure a single
453
+ cache instance is used throughout the agent run.
454
+
455
+ Attributes:
456
+ market_id_to_questions: A dictionary mapping market IDs to lists of SeerMarketQuestions
457
+ seer_subgraph_handler: Handler for interacting with the Seer subgraph
458
+ """
459
+
460
+ def __init__(self, seer_subgraph_handler: SeerSubgraphHandler | None = None):
461
+ self.market_id_to_questions: dict[
462
+ HexBytes, list[SeerMarketQuestions]
463
+ ] = defaultdict(list)
464
+ self.seer_subgraph_handler = seer_subgraph_handler or SeerSubgraphHandler()
465
+
466
+ def fetch_questions(self, market_ids: list[HexBytes]) -> None:
467
+ filtered_list = [
468
+ market_id
469
+ for market_id in market_ids
470
+ if market_id not in self.market_id_to_questions
471
+ ]
472
+ if not filtered_list:
473
+ return
474
+
475
+ questions = self.seer_subgraph_handler.get_questions_for_markets(filtered_list)
476
+ # Group questions by market_id
477
+ questions_by_market: dict[HexBytes, list[SeerMarketQuestions]] = defaultdict(
478
+ list
479
+ )
480
+ for q in questions:
481
+ questions_by_market[q.market.id].append(q)
482
+
483
+ # Update the cache with the new questions for each market
484
+ for market_id, market_questions in questions_by_market.items():
485
+ self.market_id_to_questions[market_id] = market_questions