prediction-market-agent-tooling 0.66.4__py3-none-any.whl → 0.66.6__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 (26) hide show
  1. prediction_market_agent_tooling/deploy/agent.py +23 -5
  2. prediction_market_agent_tooling/markets/agent_market.py +9 -2
  3. prediction_market_agent_tooling/markets/manifold/manifold.py +3 -2
  4. prediction_market_agent_tooling/markets/markets.py +5 -5
  5. prediction_market_agent_tooling/markets/metaculus/metaculus.py +4 -2
  6. prediction_market_agent_tooling/markets/omen/omen.py +5 -2
  7. prediction_market_agent_tooling/markets/polymarket/api.py +87 -104
  8. prediction_market_agent_tooling/markets/polymarket/data_models.py +60 -14
  9. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -54
  10. prediction_market_agent_tooling/markets/polymarket/polymarket.py +109 -26
  11. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +49 -0
  12. prediction_market_agent_tooling/markets/polymarket/utils.py +0 -21
  13. prediction_market_agent_tooling/markets/seer/seer.py +25 -26
  14. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +48 -35
  15. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +10 -0
  16. prediction_market_agent_tooling/tools/cow/cow_order.py +2 -0
  17. prediction_market_agent_tooling/tools/httpx_cached_client.py +13 -6
  18. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +7 -0
  19. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
  20. prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
  21. prediction_market_agent_tooling/tools/utils.py +5 -2
  22. {prediction_market_agent_tooling-0.66.4.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/METADATA +1 -1
  23. {prediction_market_agent_tooling-0.66.4.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/RECORD +26 -24
  24. {prediction_market_agent_tooling-0.66.4.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/LICENSE +0 -0
  25. {prediction_market_agent_tooling-0.66.4.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/WHEEL +0 -0
  26. {prediction_market_agent_tooling-0.66.4.dist-info → prediction_market_agent_tooling-0.66.6.dist-info}/entry_points.txt +0 -0
@@ -1,22 +1,35 @@
1
1
  import typing as t
2
2
 
3
- from prediction_market_agent_tooling.gtypes import USD, CollateralToken, OutcomeStr
3
+ from prediction_market_agent_tooling.gtypes import (
4
+ USD,
5
+ CollateralToken,
6
+ HexBytes,
7
+ OutcomeStr,
8
+ Probability,
9
+ )
4
10
  from prediction_market_agent_tooling.markets.agent_market import (
5
11
  AgentMarket,
6
12
  FilterBy,
7
13
  MarketFees,
14
+ MarketType,
8
15
  SortBy,
9
16
  )
17
+ from prediction_market_agent_tooling.markets.data_models import Resolution
10
18
  from prediction_market_agent_tooling.markets.polymarket.api import (
11
- get_polymarket_binary_markets,
19
+ PolymarketOrderByEnum,
20
+ get_polymarkets_with_pagination,
12
21
  )
13
22
  from prediction_market_agent_tooling.markets.polymarket.data_models import (
14
- PolymarketMarketWithPrices,
23
+ PolymarketGammaResponseDataItem,
15
24
  )
16
25
  from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
17
26
  POLYMARKET_BASE_URL,
18
27
  )
19
- from prediction_market_agent_tooling.tools.utils import DatetimeUTC
28
+ from prediction_market_agent_tooling.markets.polymarket.polymarket_subgraph_handler import (
29
+ ConditionSubgraphModel,
30
+ PolymarketSubgraphHandler,
31
+ )
32
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
20
33
 
21
34
 
22
35
  class PolymarketAgentMarket(AgentMarket):
@@ -33,19 +46,68 @@ class PolymarketAgentMarket(AgentMarket):
33
46
  fees: MarketFees = MarketFees.get_zero_fees()
34
47
 
35
48
  @staticmethod
36
- def from_data_model(model: PolymarketMarketWithPrices) -> "PolymarketAgentMarket":
49
+ def build_resolution_from_condition(
50
+ condition_id: HexBytes,
51
+ condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
52
+ outcomes: list[OutcomeStr],
53
+ ) -> Resolution | None:
54
+ condition_model = condition_model_dict.get(condition_id)
55
+ if (
56
+ not condition_model
57
+ or condition_model.resolutionTimestamp is None
58
+ or not condition_model.payoutNumerators
59
+ or not condition_model.payoutDenominator
60
+ ):
61
+ return None
62
+
63
+ # Currently we only support binary markets, hence we throw an error if we get something else.
64
+ payout_numerator_indices_gt_0 = [
65
+ idx
66
+ for idx, value in enumerate(condition_model.payoutNumerators)
67
+ if value > 0
68
+ ]
69
+ # For a binary market, there should be exactly one payout numerator greater than 0.
70
+ if len(payout_numerator_indices_gt_0) != 1:
71
+ raise ValueError(
72
+ f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators}"
73
+ )
74
+
75
+ # we return the only payout numerator greater than 0 as resolution
76
+ resolved_outcome = outcomes[payout_numerator_indices_gt_0[0]]
77
+ return Resolution.from_answer(resolved_outcome)
78
+
79
+ @staticmethod
80
+ def from_data_model(
81
+ model: PolymarketGammaResponseDataItem,
82
+ condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
83
+ ) -> "PolymarketAgentMarket":
84
+ # If len(model.markets) > 0, this denotes a categorical market.
85
+
86
+ outcomes = model.markets[0].outcomes_list
87
+ outcome_prices = model.markets[0].outcome_prices
88
+ if not outcome_prices:
89
+ # We give random prices
90
+ outcome_prices = [0.5, 0.5]
91
+ probabilities = {o: Probability(op) for o, op in zip(outcomes, outcome_prices)}
92
+
93
+ resolution = PolymarketAgentMarket.build_resolution_from_condition(
94
+ condition_id=model.markets[0].conditionId,
95
+ condition_model_dict=condition_model_dict,
96
+ outcomes=outcomes,
97
+ )
98
+
37
99
  return PolymarketAgentMarket(
38
100
  id=model.id,
39
- question=model.question,
101
+ question=model.title,
40
102
  description=model.description,
41
- outcomes=[x.outcome for x in model.tokens],
42
- resolution=model.resolution,
43
- created_time=None,
44
- close_time=model.end_date_iso,
103
+ outcomes=outcomes,
104
+ resolution=resolution,
105
+ created_time=model.startDate,
106
+ close_time=model.endDate,
45
107
  url=model.url,
46
- volume=None,
108
+ volume=CollateralToken(model.volume) if model.volume else None,
47
109
  outcome_token_pool=None,
48
- probabilities={}, # ToDo - Implement when fixing Polymarket
110
+ probabilities=probabilities,
49
111
  )
50
112
 
51
113
  def get_tiny_bet_amount(self) -> CollateralToken:
@@ -61,16 +123,11 @@ class PolymarketAgentMarket(AgentMarket):
61
123
  filter_by: FilterBy = FilterBy.OPEN,
62
124
  created_after: t.Optional[DatetimeUTC] = None,
63
125
  excluded_questions: set[str] | None = None,
64
- fetch_categorical_markets: bool = False,
65
- fetch_scalar_markets: bool = False,
126
+ market_type: MarketType = MarketType.ALL,
127
+ include_conditional_markets: bool = False,
66
128
  ) -> t.Sequence["PolymarketAgentMarket"]:
67
- if sort_by != SortBy.NONE:
68
- raise ValueError(f"Unsuported sort_by {sort_by} for Polymarket.")
69
-
70
- if created_after is not None:
71
- raise ValueError(f"Unsuported created_after for Polymarket.")
72
-
73
129
  closed: bool | None
130
+
74
131
  if filter_by == FilterBy.OPEN:
75
132
  closed = False
76
133
  elif filter_by == FilterBy.RESOLVED:
@@ -80,11 +137,37 @@ class PolymarketAgentMarket(AgentMarket):
80
137
  else:
81
138
  raise ValueError(f"Unknown filter_by: {filter_by}")
82
139
 
140
+ ascending: bool = False # default value
141
+ match sort_by:
142
+ case SortBy.NEWEST:
143
+ order_by = PolymarketOrderByEnum.START_DATE
144
+ case SortBy.CLOSING_SOONEST:
145
+ ascending = True
146
+ order_by = PolymarketOrderByEnum.END_DATE
147
+ case SortBy.HIGHEST_LIQUIDITY:
148
+ order_by = PolymarketOrderByEnum.LIQUIDITY
149
+ case SortBy.NONE:
150
+ order_by = PolymarketOrderByEnum.VOLUME_24HR
151
+ case _:
152
+ raise ValueError(f"Unknown sort_by: {sort_by}")
153
+
154
+ # closed markets also have property active=True, hence ignoring active.
155
+ markets = get_polymarkets_with_pagination(
156
+ limit=limit,
157
+ closed=closed,
158
+ order_by=order_by,
159
+ ascending=ascending,
160
+ created_after=created_after,
161
+ excluded_questions=excluded_questions,
162
+ only_binary=market_type is not MarketType.CATEGORICAL,
163
+ )
164
+
165
+ condition_models = PolymarketSubgraphHandler().get_conditions(
166
+ condition_ids=[market.markets[0].conditionId for market in markets]
167
+ )
168
+ condition_models_dict = {c.id: c for c in condition_models}
169
+
83
170
  return [
84
- PolymarketAgentMarket.from_data_model(m)
85
- for m in get_polymarket_binary_markets(
86
- limit=limit,
87
- closed=closed,
88
- excluded_questions=excluded_questions,
89
- )
171
+ PolymarketAgentMarket.from_data_model(m, condition_models_dict)
172
+ for m in markets
90
173
  ]
@@ -0,0 +1,49 @@
1
+ from pydantic import BaseModel
2
+
3
+ from prediction_market_agent_tooling.gtypes import HexBytes
4
+ from prediction_market_agent_tooling.markets.base_subgraph_handler import (
5
+ BaseSubgraphHandler,
6
+ )
7
+
8
+
9
+ class ConditionSubgraphModel(BaseModel):
10
+ id: HexBytes
11
+ payoutDenominator: int | None = None
12
+ payoutNumerators: list[int] | None = None
13
+ outcomeSlotCount: int
14
+ resolutionTimestamp: int | None = None
15
+
16
+
17
+ class PolymarketSubgraphHandler(BaseSubgraphHandler):
18
+ POLYMARKET_CONDITIONS_SUBGRAPH = "https://gateway.thegraph.com/api/{graph_api_key}/subgraphs/id/81Dm16JjuFSrqz813HysXoUPvzTwE7fsfPk2RTf66nyC"
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__()
22
+
23
+ # Load the subgraph
24
+ self.conditions_subgraph = self.sg.load_subgraph(
25
+ self.POLYMARKET_CONDITIONS_SUBGRAPH.format(
26
+ graph_api_key=self.keys.graph_api_key.get_secret_value()
27
+ )
28
+ )
29
+
30
+ def get_conditions(
31
+ self, condition_ids: list[HexBytes]
32
+ ) -> list[ConditionSubgraphModel]:
33
+ where_stms = {"id_in": [i.hex() for i in condition_ids]}
34
+ conditions = self.conditions_subgraph.Query.conditions(
35
+ where=where_stms,
36
+ )
37
+
38
+ condition_fields = [
39
+ conditions.id,
40
+ conditions.payoutNumerators,
41
+ conditions.payoutDenominator,
42
+ conditions.outcomeSlotCount,
43
+ conditions.resolutionTimestamp,
44
+ ]
45
+
46
+ conditions_models = self.do_query(
47
+ fields=condition_fields, pydantic_model=ConditionSubgraphModel
48
+ )
49
+ return conditions_models
@@ -1,28 +1,7 @@
1
- from prediction_market_agent_tooling.markets.data_models import Resolution
2
1
  from prediction_market_agent_tooling.markets.markets import MarketType
3
- from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
4
- PolymarketFullMarket,
5
- )
6
2
  from prediction_market_agent_tooling.tools.google_utils import search_google_gcp
7
3
 
8
4
 
9
- def find_resolution_on_polymarket(question: str) -> Resolution | None:
10
- full_market = find_full_polymarket(question)
11
- # TODO: Only main markets are supported right now, add logic for others if needed.
12
- return (
13
- full_market.main_market.resolution
14
- if full_market and full_market.is_main_market
15
- else None
16
- )
17
-
18
-
19
- def find_full_polymarket(question: str) -> PolymarketFullMarket | None:
20
- polymarket_url = find_url_to_polymarket(question)
21
- return (
22
- PolymarketFullMarket.fetch_from_url(polymarket_url) if polymarket_url else None
23
- )
24
-
25
-
26
5
  def find_url_to_polymarket(question: str) -> str | None:
27
6
  # Manually create potential Polymarket's slug from the question.
28
7
  replace_chars = {
@@ -24,6 +24,8 @@ from prediction_market_agent_tooling.loggers import logger
24
24
  from prediction_market_agent_tooling.markets.agent_market import (
25
25
  AgentMarket,
26
26
  FilterBy,
27
+ MarketFees,
28
+ MarketType,
27
29
  ProcessedMarket,
28
30
  ProcessedTradedMarket,
29
31
  SortBy,
@@ -76,6 +78,9 @@ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
76
78
  from prediction_market_agent_tooling.tools.tokens.auto_deposit import (
77
79
  auto_deposit_collateral_token,
78
80
  )
81
+ from prediction_market_agent_tooling.tools.tokens.slippage import (
82
+ get_slippage_tolerance_per_token,
83
+ )
79
84
  from prediction_market_agent_tooling.tools.tokens.usd import (
80
85
  get_token_in_usd,
81
86
  get_usd_in_token,
@@ -95,6 +100,7 @@ class SeerAgentMarket(AgentMarket):
95
100
  None # Seer markets don't have a description, so just default to None.
96
101
  )
97
102
  outcomes_supply: int
103
+ minimum_market_liquidity_required: CollateralToken = CollateralToken(1)
98
104
 
99
105
  def get_collateral_token_contract(
100
106
  self, web3: Web3 | None = None
@@ -140,17 +146,20 @@ class SeerAgentMarket(AgentMarket):
140
146
  logger.warning(
141
147
  f"Could not get quote for {self.collateral_token_contract_address_checksummed} from Cow, exception {e=}. Falling back to pools. "
142
148
  )
143
- token_price = self.get_colateral_price_from_pools()
144
- if token_price is None:
149
+ usd_token_price = self.get_collateral_price_from_pools()
150
+ if usd_token_price is None:
145
151
  raise RuntimeError(
146
152
  "Both CoW and pool-fallback way of getting price failed."
147
153
  ) from e
148
- return USD(x.value * token_price.value)
154
+ return USD(x.value * usd_token_price.value)
149
155
 
150
- def get_colateral_price_from_pools(self) -> CollateralToken | None:
156
+ def get_collateral_price_from_pools(self) -> USD | None:
151
157
  p = PriceManager.build(HexBytes(HexStr(self.id)))
152
158
  token_price = p.get_token_price_from_pools(token=SDAI_CONTRACT_ADDRESS)
153
- return token_price
159
+ if token_price:
160
+ return get_token_in_usd(token_price, SDAI_CONTRACT_ADDRESS)
161
+
162
+ return None
154
163
 
155
164
  def get_usd_in_token(self, x: USD) -> CollateralToken:
156
165
  try:
@@ -161,12 +170,12 @@ class SeerAgentMarket(AgentMarket):
161
170
  logger.warning(
162
171
  f"Could not get quote for {self.collateral_token_contract_address_checksummed} from Cow, exception {e=}. Falling back to pools. "
163
172
  )
164
- token_price = self.get_colateral_price_from_pools()
165
- if not token_price:
173
+ usd_token_price = self.get_collateral_price_from_pools()
174
+ if not usd_token_price:
166
175
  raise RuntimeError(
167
176
  "Both CoW and pool-fallback way of getting price failed."
168
177
  ) from e
169
- return CollateralToken(x.value / token_price.value)
178
+ return CollateralToken(x.value / usd_token_price.value)
170
179
 
171
180
  def get_buy_token_amount(
172
181
  self, bet_amount: USD | CollateralToken, outcome_str: OutcomeStr
@@ -228,9 +237,7 @@ class SeerAgentMarket(AgentMarket):
228
237
  def get_tiny_bet_amount(self) -> CollateralToken:
229
238
  return self.get_in_token(SEER_TINY_BET_AMOUNT)
230
239
 
231
- def get_position_else_raise(
232
- self, user_id: str, web3: Web3 | None = None
233
- ) -> ExistingPosition:
240
+ def get_position(self, user_id: str, web3: Web3 | None = None) -> ExistingPosition:
234
241
  """
235
242
  Fetches position from the user in a given market.
236
243
  We ignore the INVALID balances since we are only interested in binary outcomes.
@@ -261,15 +268,6 @@ class SeerAgentMarket(AgentMarket):
261
268
  amounts_ot=amounts_ot,
262
269
  )
263
270
 
264
- def get_position(
265
- self, user_id: str, web3: Web3 | None = None
266
- ) -> ExistingPosition | None:
267
- try:
268
- return self.get_position_else_raise(user_id=user_id, web3=web3)
269
- except Exception as e:
270
- logger.warning(f"Could not get position for user {user_id}, exception {e}")
271
- return None
272
-
273
271
  @staticmethod
274
272
  def get_user_id(api_keys: APIKeys) -> str:
275
273
  return OmenAgentMarket.get_user_id(api_keys)
@@ -406,16 +404,16 @@ class SeerAgentMarket(AgentMarket):
406
404
  filter_by: FilterBy = FilterBy.OPEN,
407
405
  created_after: t.Optional[DatetimeUTC] = None,
408
406
  excluded_questions: set[str] | None = None,
409
- fetch_categorical_markets: bool = False,
410
- fetch_scalar_markets: bool = False,
407
+ market_type: MarketType = MarketType.ALL,
408
+ include_conditional_markets: bool = False,
411
409
  ) -> t.Sequence["SeerAgentMarket"]:
412
410
  seer_subgraph = SeerSubgraphHandler()
411
+
413
412
  markets = seer_subgraph.get_markets(
414
413
  limit=limit,
415
414
  sort_by=sort_by,
416
415
  filter_by=filter_by,
417
- include_categorical_markets=fetch_categorical_markets,
418
- include_only_scalar_markets=fetch_scalar_markets,
416
+ market_type=market_type,
419
417
  include_conditional_markets=False,
420
418
  )
421
419
 
@@ -498,7 +496,7 @@ class SeerAgentMarket(AgentMarket):
498
496
 
499
497
  def has_liquidity_for_outcome(self, outcome: OutcomeStr) -> bool:
500
498
  liquidity = self.get_liquidity_for_outcome(outcome)
501
- return liquidity > CollateralToken(0)
499
+ return liquidity > self.minimum_market_liquidity_required
502
500
 
503
501
  def has_liquidity(self) -> bool:
504
502
  # We define a market as having liquidity if it has liquidity for all outcomes except for the invalid (index -1)
@@ -531,7 +529,7 @@ class SeerAgentMarket(AgentMarket):
531
529
  Returns:
532
530
  Transaction hash of the successful swap
533
531
  """
534
-
532
+ slippage_tolerance = get_slippage_tolerance_per_token(sell_token, buy_token)
535
533
  try:
536
534
  _, order = swap_tokens_waiting(
537
535
  amount_wei=amount_wei,
@@ -541,6 +539,7 @@ class SeerAgentMarket(AgentMarket):
541
539
  web3=web3,
542
540
  wait_order_complete=False,
543
541
  timeout=timedelta(minutes=2),
542
+ slippage_tolerance=slippage_tolerance,
544
543
  )
545
544
  order_metadata = asyncio.run(wait_for_order_completion(order=order))
546
545
  logger.debug(
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  import typing as t
3
+ from enum import Enum
3
4
  from typing import Any
4
5
 
5
6
  from subgrounds import FieldPath
@@ -13,7 +14,11 @@ from prediction_market_agent_tooling.deploy.constants import (
13
14
  )
14
15
  from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei
15
16
  from prediction_market_agent_tooling.loggers import logger
16
- from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
17
+ from prediction_market_agent_tooling.markets.agent_market import (
18
+ FilterBy,
19
+ MarketType,
20
+ SortBy,
21
+ )
17
22
  from prediction_market_agent_tooling.markets.base_subgraph_handler import (
18
23
  BaseSubgraphHandler,
19
24
  )
@@ -24,6 +29,14 @@ from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
24
29
  from prediction_market_agent_tooling.tools.web3_utils import unwrap_generic_value
25
30
 
26
31
 
32
+ class TemplateId(int, Enum):
33
+ """Template IDs used in Reality.eth questions."""
34
+
35
+ SCALAR = 1
36
+ CATEGORICAL = 2
37
+ MULTICATEGORICAL = 3
38
+
39
+
27
40
  class SeerSubgraphHandler(BaseSubgraphHandler):
28
41
  """
29
42
  Class responsible for handling interactions with Seer subgraphs.
@@ -70,6 +83,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
70
83
  markets_field.collateralToken,
71
84
  markets_field.upperBound,
72
85
  markets_field.lowerBound,
86
+ markets_field.templateId,
73
87
  ]
74
88
  return fields
75
89
 
@@ -100,8 +114,7 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
100
114
  filter_by: FilterBy,
101
115
  outcome_supply_gt_if_open: Wei,
102
116
  include_conditional_markets: bool = False,
103
- include_categorical_markets: bool = True,
104
- include_only_scalar_markets: bool = False,
117
+ market_type: MarketType = MarketType.ALL,
105
118
  ) -> dict[Any, Any]:
106
119
  now = to_int_timestamp(utcnow())
107
120
 
@@ -123,43 +136,47 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
123
136
  if not include_conditional_markets:
124
137
  and_stms["parentMarket"] = ADDRESS_ZERO.lower()
125
138
 
126
- yes_stms, no_stms = {}, {}
127
- exclude_scalar_yes, exclude_scalar_no = {}, {}
139
+ outcome_filters: list[dict[str, t.Any]] = []
128
140
 
129
- # Return scalar markets.
130
- if include_only_scalar_markets:
131
- # We are interested in scalar markets only - this excludes categorical markets
132
- yes_stms = SeerSubgraphHandler._create_case_variations_condition(
141
+ if market_type == MarketType.SCALAR:
142
+ # Template ID "1" + UP/DOWN outcomes for scalar markets
143
+ and_stms["templateId"] = TemplateId.SCALAR.value
144
+ up_filter = SeerSubgraphHandler._create_case_variations_condition(
133
145
  UP_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
134
146
  )
135
- no_stms = SeerSubgraphHandler._create_case_variations_condition(
147
+ down_filter = SeerSubgraphHandler._create_case_variations_condition(
136
148
  DOWN_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
137
149
  )
138
- elif include_conditional_markets and not include_categorical_markets:
139
- # We are interested in binary markets only
140
- yes_stms = SeerSubgraphHandler._create_case_variations_condition(
150
+ outcome_filters.extend([up_filter, down_filter])
151
+
152
+ elif market_type == MarketType.BINARY:
153
+ # Template ID "2" + YES/NO outcomes for binary markets
154
+ and_stms["templateId"] = TemplateId.CATEGORICAL.value
155
+ yes_filter = SeerSubgraphHandler._create_case_variations_condition(
141
156
  YES_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
142
157
  )
143
- no_stms = SeerSubgraphHandler._create_case_variations_condition(
158
+ no_filter = SeerSubgraphHandler._create_case_variations_condition(
144
159
  NO_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
145
160
  )
161
+ outcome_filters.extend([yes_filter, no_filter])
146
162
 
147
- if (
148
- not include_only_scalar_markets
149
- or include_categorical_markets
150
- or include_conditional_markets
151
- ):
152
- # We should not provide any scalar markets because they are exclusive for categorical markets
153
- exclude_scalar_yes = SeerSubgraphHandler._create_case_variations_condition(
154
- UP_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_not_contains", "and"
155
- )
156
- exclude_scalar_no = SeerSubgraphHandler._create_case_variations_condition(
157
- DOWN_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_not_contains", "and"
163
+ elif market_type == MarketType.CATEGORICAL:
164
+ # Template ID 2 (categorical) OR Template ID 3 (multi-categorical,
165
+ # we treat them as categorical for now for simplicity)
166
+ # https://reality.eth.limo/app/docs/html/contracts.html#templates
167
+ outcome_filters.append(
168
+ {
169
+ "or": [
170
+ {"templateId": TemplateId.CATEGORICAL.value},
171
+ {"templateId": TemplateId.MULTICATEGORICAL.value},
172
+ ]
173
+ }
158
174
  )
159
175
 
160
- where_stms: dict[str, t.Any] = {
161
- "and": [and_stms, yes_stms, no_stms, exclude_scalar_yes, exclude_scalar_no]
162
- }
176
+ # If none specified, don't add any template/outcome filters (returns all types)
177
+
178
+ all_filters = [and_stms] + outcome_filters if and_stms else outcome_filters
179
+ where_stms: dict[str, t.Any] = {"and": all_filters}
163
180
  return where_stms
164
181
 
165
182
  def _build_sort_params(
@@ -194,20 +211,16 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
194
211
  sort_by: SortBy = SortBy.NONE,
195
212
  limit: int | None = None,
196
213
  outcome_supply_gt_if_open: Wei = Wei(0),
197
- include_conditional_markets: bool = True,
198
- include_categorical_markets: bool = True,
199
- include_only_scalar_markets: bool = False,
214
+ market_type: MarketType = MarketType.ALL,
215
+ include_conditional_markets: bool = False,
200
216
  ) -> list[SeerMarket]:
201
217
  sort_direction, sort_by_field = self._build_sort_params(sort_by)
202
218
 
203
- """Returns markets that contain 2 categories plus an invalid outcome."""
204
- # Binary markets on Seer contain 3 outcomes: OutcomeA, outcomeB and an Invalid option.
205
219
  where_stms = self._build_where_statements(
206
220
  filter_by=filter_by,
207
221
  outcome_supply_gt_if_open=outcome_supply_gt_if_open,
208
222
  include_conditional_markets=include_conditional_markets,
209
- include_categorical_markets=include_categorical_markets,
210
- include_only_scalar_markets=include_only_scalar_markets,
223
+ market_type=market_type,
211
224
  )
212
225
 
213
226
  # These values can not be set to `None`, but they can be omitted.
@@ -19,6 +19,7 @@ from prediction_market_agent_tooling.markets.seer.seer_contracts import (
19
19
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
20
20
  SeerSubgraphHandler,
21
21
  )
22
+ from prediction_market_agent_tooling.tools.contract import ContractERC20OnGnosisChain
22
23
 
23
24
 
24
25
  class SwapPoolHandler:
@@ -90,6 +91,15 @@ class SwapPoolHandler:
90
91
  amount_out_minimum=amount_out_minimum,
91
92
  )
92
93
 
94
+ # make sure user has enough tokens to sell
95
+ balance_collateral_token = ContractERC20OnGnosisChain(
96
+ address=token_in
97
+ ).balanceOf(self.api_keys.bet_from_address, web3=web3)
98
+ if balance_collateral_token < amount_wei:
99
+ raise ValueError(
100
+ f"Balance {balance_collateral_token} of {token_in} insufficient for trade, required {amount_wei}"
101
+ )
102
+
93
103
  tx_receipt = SwaprRouterContract().exact_input_single(
94
104
  api_keys=self.api_keys, params=p, web3=web3
95
105
  )
@@ -211,6 +211,7 @@ def swap_tokens_waiting(
211
211
  web3: Web3 | None = None,
212
212
  wait_order_complete: bool = True,
213
213
  timeout: timedelta = timedelta(seconds=120),
214
+ slippage_tolerance: float = 0.01,
214
215
  ) -> tuple[OrderMetaData | None, CompletedOrder]:
215
216
  # CoW library uses async, so we need to wrap the call in asyncio.run for us to use it.
216
217
  return asyncio.run(
@@ -224,6 +225,7 @@ def swap_tokens_waiting(
224
225
  timeout=timeout,
225
226
  web3=web3,
226
227
  wait_order_complete=wait_order_complete,
228
+ slippage_tolerance=slippage_tolerance,
227
229
  )
228
230
  )
229
231
 
@@ -1,14 +1,21 @@
1
1
  import hishel
2
+ import httpx
2
3
 
4
+ from prediction_market_agent_tooling.tools.singleton import SingletonMeta
3
5
 
4
- class HttpxCachedClient:
5
- def __init__(self) -> None:
6
+ ONE_DAY_IN_SECONDS = 60 * 60 * 24
7
+
8
+
9
+ class HttpxCachedClient(metaclass=SingletonMeta):
10
+ def __init__(self, ttl: int = ONE_DAY_IN_SECONDS) -> None:
6
11
  storage = hishel.FileStorage(
7
- ttl=24 * 60 * 60,
8
- check_ttl_every=1 * 60 * 60,
12
+ ttl=ttl,
13
+ check_ttl_every=60,
9
14
  )
10
15
  controller = hishel.Controller(force_cache=True)
11
- self.client = hishel.CacheClient(storage=storage, controller=controller)
16
+ self.client: httpx.Client = hishel.CacheClient(
17
+ storage=storage, controller=controller
18
+ )
12
19
 
13
- def get_client(self) -> hishel.CacheClient:
20
+ def get_client(self) -> httpx.Client:
14
21
  return self.client
@@ -14,6 +14,9 @@ from prediction_market_agent_tooling.tools.cow.cow_order import (
14
14
  swap_tokens_waiting,
15
15
  )
16
16
  from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
17
+ from prediction_market_agent_tooling.tools.tokens.slippage import (
18
+ get_slippage_tolerance_per_token,
19
+ )
17
20
  from prediction_market_agent_tooling.tools.tokens.usd import get_usd_in_token
18
21
  from prediction_market_agent_tooling.tools.utils import should_not_happen
19
22
 
@@ -156,10 +159,14 @@ def auto_deposit_erc20(
156
159
  raise ValueError(
157
160
  "Not enough of the source token to sell to get the desired amount of the collateral token."
158
161
  )
162
+ slippage_tolerance = get_slippage_tolerance_per_token(
163
+ KEEPING_ERC20_TOKEN.address, collateral_token_contract.address
164
+ )
159
165
  swap_tokens_waiting(
160
166
  amount_wei=amount_to_sell_wei,
161
167
  sell_token=KEEPING_ERC20_TOKEN.address,
162
168
  buy_token=collateral_token_contract.address,
163
169
  api_keys=api_keys,
164
170
  web3=web3,
171
+ slippage_tolerance=slippage_tolerance,
165
172
  )
@@ -9,6 +9,9 @@ from prediction_market_agent_tooling.tools.contract import (
9
9
  )
10
10
  from prediction_market_agent_tooling.tools.cow.cow_order import swap_tokens_waiting
11
11
  from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
12
+ from prediction_market_agent_tooling.tools.tokens.slippage import (
13
+ get_slippage_tolerance_per_token,
14
+ )
12
15
  from prediction_market_agent_tooling.tools.utils import should_not_happen
13
16
 
14
17
 
@@ -49,12 +52,17 @@ def auto_withdraw_collateral_token(
49
52
  f"Swapping {amount_wei.as_token} from {collateral_token_contract.symbol_cached(web3)} into {KEEPING_ERC20_TOKEN.symbol_cached(web3)}"
50
53
  )
51
54
  # Otherwise, DEX will handle the rest of token swaps.
55
+ slippage_tolerance = get_slippage_tolerance_per_token(
56
+ collateral_token_contract.address,
57
+ KEEPING_ERC20_TOKEN.address,
58
+ )
52
59
  swap_tokens_waiting(
53
60
  amount_wei=amount_wei,
54
61
  sell_token=collateral_token_contract.address,
55
62
  buy_token=KEEPING_ERC20_TOKEN.address,
56
63
  api_keys=api_keys,
57
64
  web3=web3,
65
+ slippage_tolerance=slippage_tolerance,
58
66
  )
59
67
  else:
60
68
  should_not_happen("Unsupported ERC20 contract type.")