prediction-market-agent-tooling 0.64.12.dev660__py3-none-any.whl → 0.65.0__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 (51) hide show
  1. prediction_market_agent_tooling/benchmark/agents.py +19 -16
  2. prediction_market_agent_tooling/benchmark/benchmark.py +94 -84
  3. prediction_market_agent_tooling/benchmark/utils.py +8 -9
  4. prediction_market_agent_tooling/deploy/agent.py +85 -125
  5. prediction_market_agent_tooling/deploy/agent_example.py +20 -10
  6. prediction_market_agent_tooling/deploy/betting_strategy.py +222 -96
  7. prediction_market_agent_tooling/deploy/constants.py +4 -0
  8. prediction_market_agent_tooling/jobs/jobs_models.py +15 -4
  9. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +3 -3
  10. prediction_market_agent_tooling/markets/agent_market.py +145 -50
  11. prediction_market_agent_tooling/markets/blockchain_utils.py +10 -1
  12. prediction_market_agent_tooling/markets/data_models.py +83 -17
  13. prediction_market_agent_tooling/markets/manifold/api.py +18 -7
  14. prediction_market_agent_tooling/markets/manifold/data_models.py +23 -16
  15. prediction_market_agent_tooling/markets/manifold/manifold.py +18 -18
  16. prediction_market_agent_tooling/markets/manifold/utils.py +7 -12
  17. prediction_market_agent_tooling/markets/markets.py +2 -1
  18. prediction_market_agent_tooling/markets/metaculus/metaculus.py +29 -4
  19. prediction_market_agent_tooling/markets/omen/data_models.py +17 -32
  20. prediction_market_agent_tooling/markets/omen/omen.py +65 -108
  21. prediction_market_agent_tooling/markets/omen/omen_contracts.py +2 -5
  22. prediction_market_agent_tooling/markets/omen/omen_resolving.py +13 -13
  23. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +18 -12
  24. prediction_market_agent_tooling/markets/polymarket/data_models.py +7 -3
  25. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +7 -3
  26. prediction_market_agent_tooling/markets/polymarket/polymarket.py +5 -4
  27. prediction_market_agent_tooling/markets/seer/data_models.py +0 -83
  28. prediction_market_agent_tooling/markets/seer/price_manager.py +44 -30
  29. prediction_market_agent_tooling/markets/seer/seer.py +105 -105
  30. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +34 -41
  31. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +1 -1
  32. prediction_market_agent_tooling/tools/cow/cow_order.py +10 -3
  33. prediction_market_agent_tooling/tools/is_predictable.py +2 -3
  34. prediction_market_agent_tooling/tools/langfuse_client_utils.py +4 -4
  35. prediction_market_agent_tooling/tools/omen/sell_positions.py +3 -2
  36. prediction_market_agent_tooling/tools/utils.py +26 -13
  37. {prediction_market_agent_tooling-0.64.12.dev660.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/METADATA +2 -2
  38. {prediction_market_agent_tooling-0.64.12.dev660.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/RECORD +41 -51
  39. prediction_market_agent_tooling/monitor/financial_metrics/financial_metrics.py +0 -68
  40. prediction_market_agent_tooling/monitor/markets/manifold.py +0 -90
  41. prediction_market_agent_tooling/monitor/markets/metaculus.py +0 -43
  42. prediction_market_agent_tooling/monitor/markets/omen.py +0 -88
  43. prediction_market_agent_tooling/monitor/markets/polymarket.py +0 -49
  44. prediction_market_agent_tooling/monitor/monitor.py +0 -406
  45. prediction_market_agent_tooling/monitor/monitor_app.py +0 -149
  46. prediction_market_agent_tooling/monitor/monitor_settings.py +0 -27
  47. prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +0 -146
  48. prediction_market_agent_tooling/tools/betting_strategies/minimum_bet_to_win.py +0 -12
  49. {prediction_market_agent_tooling-0.64.12.dev660.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/LICENSE +0 -0
  50. {prediction_market_agent_tooling-0.64.12.dev660.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/WHEEL +0 -0
  51. {prediction_market_agent_tooling-0.64.12.dev660.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/entry_points.txt +0 -0
@@ -181,19 +181,23 @@ class Market(BaseModel):
181
181
  zip(self.outcomes, self.outcomePrices)
182
182
  )
183
183
 
184
- # On Polymarket, we can find out binary market resolution by by checking for the outcome prices.
184
+ # On Polymarket, we can find out binary market resolution by checking for the outcome prices.
185
185
  # E.g. if `Yes` price (probability) is 1$ and `No` price (probability) is 0$, it means the resolution is `Yes`.
186
186
  if (
187
187
  outcome_to_outcome_price[POLYMARKET_TRUE_OUTCOME] == 1.0
188
188
  and outcome_to_outcome_price[POLYMARKET_FALSE_OUTCOME] == 0.0
189
189
  ):
190
- return Resolution.YES
190
+ return Resolution(
191
+ outcome=OutcomeStr(POLYMARKET_TRUE_OUTCOME), invalid=False
192
+ )
191
193
 
192
194
  elif (
193
195
  outcome_to_outcome_price[POLYMARKET_TRUE_OUTCOME] == 0.0
194
196
  and outcome_to_outcome_price[POLYMARKET_FALSE_OUTCOME] == 1.0
195
197
  ):
196
- return Resolution.NO
198
+ return Resolution(
199
+ outcome=OutcomeStr(POLYMARKET_FALSE_OUTCOME), invalid=False
200
+ )
197
201
 
198
202
  else:
199
203
  raise ValueError(
@@ -1,6 +1,6 @@
1
1
  import typing as t
2
2
 
3
- from prediction_market_agent_tooling.gtypes import USD, CollateralToken
3
+ from prediction_market_agent_tooling.gtypes import USD, CollateralToken, OutcomeStr
4
4
  from prediction_market_agent_tooling.markets.agent_market import (
5
5
  AgentMarket,
6
6
  FilterBy,
@@ -40,27 +40,28 @@ class PolymarketAgentMarket(AgentMarket):
40
40
  description=model.description,
41
41
  outcomes=[x.outcome for x in model.tokens],
42
42
  resolution=model.resolution,
43
- current_p_yes=model.p_yes,
44
43
  created_time=None,
45
44
  close_time=model.end_date_iso,
46
45
  url=model.url,
47
46
  volume=None,
48
47
  outcome_token_pool=None,
48
+ probabilities={}, # ToDo - Implement when fixing Polymarket
49
49
  )
50
50
 
51
51
  def get_tiny_bet_amount(self) -> CollateralToken:
52
52
  raise NotImplementedError("TODO: Implement to allow betting on Polymarket.")
53
53
 
54
- def place_bet(self, outcome: bool, amount: USD) -> str:
54
+ def place_bet(self, outcome: OutcomeStr, amount: USD) -> str:
55
55
  raise NotImplementedError("TODO: Implement to allow betting on Polymarket.")
56
56
 
57
57
  @staticmethod
58
- def get_binary_markets(
58
+ def get_markets(
59
59
  limit: int,
60
60
  sort_by: SortBy = SortBy.NONE,
61
61
  filter_by: FilterBy = FilterBy.OPEN,
62
62
  created_after: t.Optional[DatetimeUTC] = None,
63
63
  excluded_questions: set[str] | None = None,
64
+ fetch_categorical_markets: bool = False,
64
65
  ) -> t.Sequence["PolymarketAgentMarket"]:
65
66
  if sort_by != SortBy.NONE:
66
67
  raise ValueError(f"Unsuported sort_by {sort_by} for Polymarket.")
@@ -1,6 +1,4 @@
1
- import re
2
1
  import typing as t
3
- from enum import Enum
4
2
  from urllib.parse import urljoin
5
3
 
6
4
  from pydantic import BaseModel, ConfigDict, Field
@@ -16,7 +14,6 @@ from prediction_market_agent_tooling.gtypes import (
16
14
  OutcomeWei,
17
15
  Web3Wei,
18
16
  )
19
- from prediction_market_agent_tooling.markets.data_models import Resolution
20
17
  from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
21
18
  SeerParentMarket,
22
19
  )
@@ -47,44 +44,6 @@ class CreateCategoricalMarketsParams(BaseModel):
47
44
  token_names: list[str] = Field(..., alias="tokenNames")
48
45
 
49
46
 
50
- class SeerOutcomeEnum(str, Enum):
51
- YES = "yes"
52
- NO = "no"
53
- INVALID = "invalid"
54
-
55
- @classmethod
56
- def from_bool(cls, value: bool) -> "SeerOutcomeEnum":
57
- return cls.YES if value else cls.NO
58
-
59
- @classmethod
60
- def from_string(cls, value: str) -> "SeerOutcomeEnum":
61
- """Convert a string (case-insensitive) to an Outcome enum."""
62
- normalized = value.strip().lower()
63
- patterns = {
64
- r"^yes$": cls.YES,
65
- r"^no$": cls.NO,
66
- r"^(invalid|invalid result)$": cls.INVALID,
67
- }
68
-
69
- # Search through patterns and return the first match
70
- for pattern, outcome in patterns.items():
71
- if re.search(pattern, normalized):
72
- return outcome
73
-
74
- raise ValueError(f"Could not map {value=} to an outcome.")
75
-
76
- def to_bool(self) -> bool:
77
- """Convert a SeerOutcomeEnum to a boolean value."""
78
- if self == self.YES:
79
- return True
80
- elif self == self.NO:
81
- return False
82
- elif self == self.INVALID:
83
- raise ValueError("Cannot convert INVALID outcome to boolean.")
84
- else:
85
- raise ValueError(f"Unknown outcome: {self}")
86
-
87
-
88
47
  SEER_BASE_URL = "https://app.seer.pm"
89
48
 
90
49
 
@@ -115,22 +74,8 @@ class SeerMarket(BaseModel):
115
74
  # 1. An invalid outcome AND
116
75
  # 2. Invalid payoutNumerator is 1.
117
76
 
118
- try:
119
- self.outcome_as_enums[SeerOutcomeEnum.INVALID]
120
- except KeyError:
121
- raise ValueError(
122
- f"Market {self.id.hex()} has no invalid outcome. {self.outcomes}"
123
- )
124
-
125
77
  return self.payout_reported and self.payout_numerators[-1] != 1
126
78
 
127
- @property
128
- def outcome_as_enums(self) -> dict[SeerOutcomeEnum, int]:
129
- return {
130
- SeerOutcomeEnum.from_string(outcome): idx
131
- for idx, outcome in enumerate(self.outcomes)
132
- }
133
-
134
79
  @property
135
80
  def is_resolved(self) -> bool:
136
81
  return self.payout_reported
@@ -139,18 +84,6 @@ class SeerMarket(BaseModel):
139
84
  def is_resolved_with_valid_answer(self) -> bool:
140
85
  return self.is_resolved and self.has_valid_answer
141
86
 
142
- def get_resolution_enum(self) -> t.Optional[Resolution]:
143
- if not self.is_resolved_with_valid_answer:
144
- return None
145
-
146
- max_idx = self.payout_numerators.index(1)
147
-
148
- outcome: str = self.outcomes[max_idx]
149
- outcome_enum = SeerOutcomeEnum.from_string(outcome)
150
- if outcome_enum.to_bool():
151
- return Resolution.YES
152
- return Resolution.NO
153
-
154
87
  def is_redeemable(self, owner: ChecksumAddress, web3: Web3 | None = None) -> bool:
155
88
  token_balances = self.get_outcome_token_balances(owner, web3)
156
89
  if not self.payout_reported:
@@ -177,22 +110,6 @@ class SeerMarket(BaseModel):
177
110
  # 3 because Seer has also third, `Invalid` outcome.
178
111
  return len(self.outcomes) == 3
179
112
 
180
- def boolean_outcome_from_answer(self, answer: HexBytes) -> bool:
181
- if not self.is_binary:
182
- raise ValueError(
183
- f"Market with title {self.title} is not binary, it has {len(self.outcomes)} outcomes."
184
- )
185
-
186
- outcome: str = self.outcomes[answer.as_int()]
187
- outcome_enum = SeerOutcomeEnum.from_string(outcome)
188
- return outcome_enum.to_bool()
189
-
190
- def get_resolution_enum_from_answer(self, answer: HexBytes) -> Resolution:
191
- if self.boolean_outcome_from_answer(answer):
192
- return Resolution.YES
193
- else:
194
- return Resolution.NO
195
-
196
113
  @property
197
114
  def collateral_token_contract_address_checksummed(self) -> ChecksumAddress:
198
115
  return Web3.to_checksum_address(self.collateral_token)
@@ -6,13 +6,12 @@ from web3 import Web3
6
6
  from prediction_market_agent_tooling.gtypes import (
7
7
  ChecksumAddress,
8
8
  CollateralToken,
9
+ HexAddress,
10
+ OutcomeStr,
9
11
  Probability,
10
12
  )
11
13
  from prediction_market_agent_tooling.loggers import logger
12
- from prediction_market_agent_tooling.markets.seer.data_models import (
13
- SeerMarket,
14
- SeerOutcomeEnum,
15
- )
14
+ from prediction_market_agent_tooling.markets.seer.data_models import SeerMarket
16
15
  from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
17
16
  SeerSubgraphHandler,
18
17
  )
@@ -65,30 +64,6 @@ class PriceManager:
65
64
  f"{price_diff_pct=} larger than {max_price_diff=} for seer market {self.seer_market.id.hex()} "
66
65
  )
67
66
 
68
- def current_p_yes(self) -> Probability | None:
69
- # Inspired by https://github.com/seer-pm/demo/blob/ca682153a6b4d4dd3dcc4ad8bdcbe32202fc8fe7/web/src/hooks/useMarketOdds.ts#L15
70
- price_data: dict[int, CollateralToken | None] = {}
71
- for idx, wrapped_token in enumerate(self.seer_market.wrapped_tokens):
72
- price = self.get_price_for_token(
73
- token=Web3.to_checksum_address(wrapped_token),
74
- )
75
-
76
- price_data[idx] = price
77
-
78
- price_yes = price_data[self.seer_market.outcome_as_enums[SeerOutcomeEnum.YES]]
79
- price_no = price_data[self.seer_market.outcome_as_enums[SeerOutcomeEnum.NO]]
80
-
81
- # We only return a probability if we have both price_yes and price_no, since we could place bets
82
- # in both sides hence we need current probabilities for both outcomes.
83
- if price_yes is not None and price_no is not None:
84
- normalized_price_yes = price_yes / (price_yes + price_no)
85
- self._log_track_price_normalization_diff(
86
- old_price=price_yes.value, normalized_price=normalized_price_yes
87
- )
88
- return Probability(normalized_price_yes)
89
- else:
90
- return None
91
-
92
67
  @cached(TTLCache(maxsize=100, ttl=5 * 60), key=_make_cache_key)
93
68
  def get_price_for_token(
94
69
  self,
@@ -117,7 +92,7 @@ class PriceManager:
117
92
  return self.get_token_price_from_pools(token=token)
118
93
 
119
94
  @staticmethod
120
- def _pool_token0_matches_token(token: ChecksumAddress, pool: SeerPool) -> bool:
95
+ def pool_token0_matches_token(token: ChecksumAddress, pool: SeerPool) -> bool:
121
96
  return pool.token0.id.hex().lower() == token.lower()
122
97
 
123
98
  def get_token_price_from_pools(
@@ -138,7 +113,46 @@ class PriceManager:
138
113
  # For example, in a outcomeYES (token0)/sDAI pool (token1), token1Price is the price of outcomeYES in units of sDAI.
139
114
  price = (
140
115
  pool.token1Price
141
- if self._pool_token0_matches_token(token=token, pool=pool)
116
+ if self.pool_token0_matches_token(token=token, pool=pool)
142
117
  else pool.token0Price
143
118
  )
144
119
  return price
120
+
121
+ def build_probability_map(self) -> dict[OutcomeStr, Probability]:
122
+ # Inspired by https://github.com/seer-pm/demo/blob/ca682153a6b4d4dd3dcc4ad8bdcbe32202fc8fe7/web/src/hooks/useMarketOdds.ts#L15
123
+ price_data: dict[HexAddress, CollateralToken] = {}
124
+
125
+ for wrapped_token in self.seer_market.wrapped_tokens:
126
+ price = self.get_price_for_token(
127
+ token=Web3.to_checksum_address(wrapped_token),
128
+ )
129
+ price_data[wrapped_token] = (
130
+ price if price is not None else CollateralToken.zero()
131
+ )
132
+
133
+ # We normalize the prices to sum up to 1.
134
+ normalized_prices = {}
135
+
136
+ if not price_data or (
137
+ sum(price_data.values(), start=CollateralToken.zero())
138
+ == CollateralToken.zero()
139
+ ):
140
+ return {
141
+ OutcomeStr(outcome): Probability(0)
142
+ for outcome in self.seer_market.outcomes
143
+ }
144
+
145
+ for outcome_token, price in price_data.items():
146
+ old_price = price
147
+ new_price = Probability(
148
+ price / (sum(price_data.values(), start=CollateralToken.zero()))
149
+ )
150
+ self._log_track_price_normalization_diff(
151
+ old_price=old_price.value, normalized_price=new_price
152
+ )
153
+ outcome = self.seer_market.outcomes[
154
+ self.seer_market.wrapped_tokens.index(outcome_token)
155
+ ]
156
+ normalized_prices[OutcomeStr(outcome)] = new_price
157
+
158
+ return normalized_prices
@@ -1,6 +1,5 @@
1
1
  import typing as t
2
2
 
3
- from eth_pydantic_types import HexStr
4
3
  from eth_typing import ChecksumAddress
5
4
  from web3 import Web3
6
5
  from web3.types import TxReceipt
@@ -11,6 +10,7 @@ from prediction_market_agent_tooling.gtypes import (
11
10
  CollateralToken,
12
11
  HexAddress,
13
12
  HexBytes,
13
+ HexStr,
14
14
  OutcomeStr,
15
15
  OutcomeToken,
16
16
  OutcomeWei,
@@ -30,7 +30,6 @@ from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
30
30
  from prediction_market_agent_tooling.markets.seer.data_models import (
31
31
  RedeemParams,
32
32
  SeerMarket,
33
- SeerOutcomeEnum,
34
33
  )
35
34
  from prediction_market_agent_tooling.markets.seer.price_manager import PriceManager
36
35
  from prediction_market_agent_tooling.markets.seer.seer_contracts import (
@@ -71,10 +70,10 @@ class SeerAgentMarket(AgentMarket):
71
70
  creator: HexAddress
72
71
  collateral_token_contract_address_checksummed: ChecksumAddress
73
72
  condition_id: HexBytes
74
- seer_outcomes: dict[SeerOutcomeEnum, int]
75
73
  description: str | None = (
76
74
  None # Seer markets don't have a description, so just default to None.
77
75
  )
76
+ outcomes_supply: int
78
77
 
79
78
  def get_collateral_token_contract(
80
79
  self, web3: Web3 | None = None
@@ -110,12 +109,17 @@ class SeerAgentMarket(AgentMarket):
110
109
  return get_usd_in_token(x, self.collateral_token_contract_address_checksummed)
111
110
 
112
111
  def get_buy_token_amount(
113
- self, bet_amount: USD | CollateralToken, direction: bool
112
+ self, bet_amount: USD | CollateralToken, outcome_str: OutcomeStr
114
113
  ) -> OutcomeToken | None:
115
114
  """Returns number of outcome tokens returned for a given bet expressed in collateral units."""
116
115
 
117
- outcome_str = self.get_outcome_str_from_bool(direction)
116
+ if outcome_str not in self.outcomes:
117
+ raise ValueError(
118
+ f"Outcome {outcome_str} not found in market outcomes {self.outcomes}"
119
+ )
120
+
118
121
  outcome_token = self.get_wrapped_token_for_outcome(outcome_str)
122
+
119
123
  bet_amount_in_tokens = self.get_in_token(bet_amount)
120
124
 
121
125
  p = PriceManager.build(market_id=HexBytes(HexStr(self.id)))
@@ -130,13 +134,12 @@ class SeerAgentMarket(AgentMarket):
130
134
  return OutcomeToken(amount_outcome_tokens)
131
135
 
132
136
  def get_sell_value_of_outcome_token(
133
- self, outcome: str, amount: OutcomeToken
137
+ self, outcome: OutcomeStr, amount: OutcomeToken
134
138
  ) -> CollateralToken:
135
139
  if amount == amount.zero():
136
140
  return CollateralToken.zero()
137
141
 
138
- outcome_index = self.get_outcome_index(outcome=outcome)
139
- wrapped_outcome_token = self.wrapped_tokens[outcome_index]
142
+ wrapped_outcome_token = self.get_wrapped_token_for_outcome(outcome)
140
143
 
141
144
  # We calculate how much collateral we would get back if we sold `amount` of outcome token.
142
145
  value_outcome_token_in_collateral = get_buy_token_amount_else_raise(
@@ -146,11 +149,6 @@ class SeerAgentMarket(AgentMarket):
146
149
  )
147
150
  return value_outcome_token_in_collateral.as_token
148
151
 
149
- def get_outcome_str_from_bool(self, outcome: bool) -> OutcomeStr:
150
- outcome_translated = SeerOutcomeEnum.from_bool(outcome)
151
- idx = self.seer_outcomes[outcome_translated]
152
- return OutcomeStr(self.outcomes[idx])
153
-
154
152
  @staticmethod
155
153
  def get_trade_balance(api_keys: APIKeys) -> USD:
156
154
  return OmenAgentMarket.get_trade_balance(api_keys=api_keys)
@@ -168,17 +166,16 @@ class SeerAgentMarket(AgentMarket):
168
166
 
169
167
  amounts_ot: dict[OutcomeStr, OutcomeToken] = {}
170
168
 
171
- for outcome in [True, False]:
172
- outcome_str = self.get_outcome_str_from_bool(outcome)
173
- wrapped_token = self.get_wrapped_token_for_outcome(outcome_str)
174
-
169
+ for outcome_str, wrapped_token in zip(self.outcomes, self.wrapped_tokens):
175
170
  outcome_token_balance_wei = OutcomeWei.from_wei(
176
171
  ContractERC20OnGnosisChain(address=wrapped_token).balanceOf(
177
172
  for_address=Web3.to_checksum_address(user_id), web3=web3
178
173
  )
179
174
  )
180
- outcome_str = self.get_outcome_str_from_bool(outcome=outcome)
181
- amounts_ot[outcome_str] = outcome_token_balance_wei.as_outcome_token
175
+
176
+ amounts_ot[
177
+ OutcomeStr(outcome_str)
178
+ ] = outcome_token_balance_wei.as_outcome_token
182
179
 
183
180
  amounts_current = {
184
181
  k: self.get_token_in_usd(self.get_sell_value_of_outcome_token(k, v))
@@ -194,76 +191,6 @@ class SeerAgentMarket(AgentMarket):
194
191
  amounts_ot=amounts_ot,
195
192
  )
196
193
 
197
- def get_outcome_str_from_idx(self, outcome_index: int) -> OutcomeStr:
198
- return self.outcomes[outcome_index]
199
-
200
- def get_liquidity_for_outcome(
201
- self, outcome: OutcomeStr, web3: Web3 | None = None
202
- ) -> CollateralToken:
203
- """Liquidity per outcome is comprised of the balance of outcomeToken + collateralToken held by the pool itself (see https://github.com/seer-pm/demo/blob/7bfd0a062780ed6567f65714c4fc4f6e6cdf1c4f/web/netlify/functions/utils/fetchPools.ts#L35-L42)."""
204
-
205
- outcome_token = self.get_wrapped_token_for_outcome(outcome)
206
- pool = SeerSubgraphHandler().get_pool_by_token(
207
- token_address=outcome_token,
208
- collateral_address=self.collateral_token_contract_address_checksummed,
209
- )
210
- if not pool:
211
- logger.info(
212
- f"Could not fetch pool for token {outcome_token}, no liquidity available for outcome."
213
- )
214
- return CollateralToken(0)
215
- p = PriceManager.build(HexBytes(HexStr(self.id)))
216
- total = CollateralToken(0)
217
-
218
- for token_address in [pool.token0.id, pool.token1.id]:
219
- token_address_checksummed = Web3.to_checksum_address(token_address)
220
- token_contract = ContractERC20OnGnosisChain(
221
- address=token_address_checksummed
222
- )
223
-
224
- token_balance = token_contract.balance_of_in_tokens(
225
- for_address=Web3.to_checksum_address(pool.id.hex()),
226
- web3=web3,
227
- )
228
-
229
- # get price
230
- token_price_in_sdai = (
231
- p.get_token_price_from_pools(token=token_address_checksummed)
232
- if token_address_checksummed
233
- != self.collateral_token_contract_address_checksummed
234
- else CollateralToken(1.0)
235
- )
236
-
237
- # We ignore the liquidity in outcome tokens if price unknown.
238
- if token_price_in_sdai:
239
- sdai_balance = token_balance * token_price_in_sdai
240
- total += sdai_balance
241
-
242
- return total
243
-
244
- def get_liquidity(self) -> CollateralToken:
245
- liquidity_in_collateral = CollateralToken(0)
246
- # We ignore the invalid outcome
247
- for outcome in self.outcomes[:-1]:
248
- liquidity_for_outcome = self.get_liquidity_for_outcome(outcome)
249
- liquidity_in_collateral += liquidity_for_outcome
250
-
251
- return liquidity_in_collateral
252
-
253
- def has_liquidity_for_outcome(self, outcome: OutcomeStr) -> bool:
254
- liquidity = self.get_liquidity_for_outcome(outcome)
255
- return liquidity > CollateralToken(0)
256
-
257
- def has_liquidity(self) -> bool:
258
- # We define a market as having liquidity if it has liquidity for all outcomes except for the invalid (index -1)
259
- return all(
260
- [self.has_liquidity_for_outcome(outcome) for outcome in self.outcomes[:-1]]
261
- )
262
-
263
- def get_wrapped_token_for_outcome(self, outcome: OutcomeStr) -> ChecksumAddress:
264
- outcome_idx = self.outcomes.index(outcome)
265
- return self.wrapped_tokens[outcome_idx]
266
-
267
194
  def get_position(
268
195
  self, user_id: str, web3: Web3 | None = None
269
196
  ) -> ExistingPosition | None:
@@ -307,7 +234,7 @@ class SeerAgentMarket(AgentMarket):
307
234
  web3 = RPCConfig().get_web3()
308
235
  subgraph = SeerSubgraphHandler()
309
236
 
310
- closed_markets = subgraph.get_binary_markets(
237
+ closed_markets = subgraph.get_markets(
311
238
  filter_by=FilterBy.RESOLVED, sort_by=SortBy.NEWEST
312
239
  )
313
240
  filtered_markets = SeerAgentMarket._filter_markets_contained_in_trades(
@@ -351,10 +278,11 @@ class SeerAgentMarket(AgentMarket):
351
278
  model: SeerMarket, seer_subgraph: SeerSubgraphHandler
352
279
  ) -> t.Optional["SeerAgentMarket"]:
353
280
  p = PriceManager(seer_market=model, seer_subgraph=seer_subgraph)
354
- current_p_yes = p.current_p_yes()
355
- if not current_p_yes:
281
+
282
+ probability_map = p.build_probability_map()
283
+ if not probability_map:
356
284
  logger.info(
357
- f"p_yes for market {model.id.hex()} could not be calculated. Skipping."
285
+ f"probability_map for market {model.id.hex()} could not be calculated. Skipping."
358
286
  )
359
287
  return None
360
288
 
@@ -371,23 +299,27 @@ class SeerAgentMarket(AgentMarket):
371
299
  wrapped_tokens=[Web3.to_checksum_address(i) for i in model.wrapped_tokens],
372
300
  fees=MarketFees.get_zero_fees(),
373
301
  outcome_token_pool=None,
374
- resolution=model.get_resolution_enum(),
302
+ outcomes_supply=model.outcomes_supply,
303
+ resolution=None,
375
304
  volume=None,
376
- current_p_yes=current_p_yes,
377
- seer_outcomes=model.outcome_as_enums,
305
+ probabilities=probability_map,
378
306
  )
379
307
 
380
308
  @staticmethod
381
- def get_binary_markets(
309
+ def get_markets(
382
310
  limit: int,
383
311
  sort_by: SortBy,
384
312
  filter_by: FilterBy = FilterBy.OPEN,
385
313
  created_after: t.Optional[DatetimeUTC] = None,
386
314
  excluded_questions: set[str] | None = None,
315
+ fetch_categorical_markets: bool = False,
387
316
  ) -> t.Sequence["SeerAgentMarket"]:
388
317
  seer_subgraph = SeerSubgraphHandler()
389
- markets = seer_subgraph.get_binary_markets(
390
- limit=limit, sort_by=sort_by, filter_by=filter_by
318
+ markets = seer_subgraph.get_markets(
319
+ limit=limit,
320
+ sort_by=sort_by,
321
+ filter_by=filter_by,
322
+ include_categorical_markets=fetch_categorical_markets,
391
323
  )
392
324
 
393
325
  # We exclude the None values below because `from_data_model_with_subgraph` can return None, which
@@ -403,9 +335,79 @@ class SeerAgentMarket(AgentMarket):
403
335
  is not None
404
336
  ]
405
337
 
338
+ def get_outcome_str_from_idx(self, outcome_index: int) -> OutcomeStr:
339
+ return self.outcomes[outcome_index]
340
+
341
+ def get_liquidity_for_outcome(
342
+ self, outcome: OutcomeStr, web3: Web3 | None = None
343
+ ) -> CollateralToken:
344
+ """Liquidity per outcome is comprised of the balance of outcomeToken + collateralToken held by the pool itself (see https://github.com/seer-pm/demo/blob/7bfd0a062780ed6567f65714c4fc4f6e6cdf1c4f/web/netlify/functions/utils/fetchPools.ts#L35-L42)."""
345
+
346
+ outcome_token = self.get_wrapped_token_for_outcome(outcome)
347
+ pool = SeerSubgraphHandler().get_pool_by_token(
348
+ token_address=outcome_token,
349
+ collateral_address=self.collateral_token_contract_address_checksummed,
350
+ )
351
+ if not pool:
352
+ logger.info(
353
+ f"Could not fetch pool for token {outcome_token}, no liquidity available for outcome."
354
+ )
355
+ return CollateralToken(0)
356
+ p = PriceManager.build(HexBytes(HexStr(self.id)))
357
+ total = CollateralToken(0)
358
+
359
+ for token_address in [pool.token0.id, pool.token1.id]:
360
+ token_address_checksummed = Web3.to_checksum_address(token_address)
361
+ token_contract = ContractERC20OnGnosisChain(
362
+ address=token_address_checksummed
363
+ )
364
+
365
+ token_balance = token_contract.balance_of_in_tokens(
366
+ for_address=Web3.to_checksum_address(HexAddress(HexStr(pool.id.hex()))),
367
+ web3=web3,
368
+ )
369
+
370
+ # get price
371
+ token_price_in_sdai = (
372
+ p.get_token_price_from_pools(token=token_address_checksummed)
373
+ if token_address_checksummed
374
+ != self.collateral_token_contract_address_checksummed
375
+ else CollateralToken(1.0)
376
+ )
377
+
378
+ # We ignore the liquidity in outcome tokens if price unknown.
379
+ if token_price_in_sdai:
380
+ sdai_balance = token_balance * token_price_in_sdai
381
+ total += sdai_balance
382
+
383
+ return total
384
+
385
+ def get_liquidity(self) -> CollateralToken:
386
+ liquidity_in_collateral = CollateralToken(0)
387
+ # We ignore the invalid outcome
388
+ for outcome in self.outcomes[:-1]:
389
+ liquidity_for_outcome = self.get_liquidity_for_outcome(outcome)
390
+ liquidity_in_collateral += liquidity_for_outcome
391
+
392
+ return liquidity_in_collateral
393
+
394
+ def has_liquidity_for_outcome(self, outcome: OutcomeStr) -> bool:
395
+ liquidity = self.get_liquidity_for_outcome(outcome)
396
+ return liquidity > CollateralToken(0)
397
+
398
+ def has_liquidity(self) -> bool:
399
+ # We define a market as having liquidity if it has liquidity for all outcomes except for the invalid (index -1)
400
+ return all(
401
+ [self.has_liquidity_for_outcome(outcome) for outcome in self.outcomes[:-1]]
402
+ )
403
+
404
+ def get_wrapped_token_for_outcome(self, outcome: OutcomeStr) -> ChecksumAddress:
405
+ outcome_idx = self.outcomes.index(outcome)
406
+ return self.wrapped_tokens[outcome_idx]
407
+
406
408
  def place_bet(
407
409
  self,
408
- outcome: bool,
410
+ outcome: OutcomeStr,
409
411
  amount: USD,
410
412
  auto_deposit: bool = True,
411
413
  web3: Web3 | None = None,
@@ -432,8 +434,7 @@ class SeerAgentMarket(AgentMarket):
432
434
  f"Balance {collateral_balance} not enough for bet size {amount}"
433
435
  )
434
436
 
435
- outcome_str = self.get_outcome_str_from_bool(outcome)
436
- outcome_token = self.get_wrapped_token_for_outcome(outcome_str)
437
+ outcome_token = self.get_wrapped_token_for_outcome(outcome)
437
438
 
438
439
  # Sell sDAI using token address
439
440
  order_metadata = swap_tokens_waiting(
@@ -451,7 +452,7 @@ class SeerAgentMarket(AgentMarket):
451
452
 
452
453
  def sell_tokens(
453
454
  self,
454
- outcome: bool,
455
+ outcome: OutcomeStr,
455
456
  amount: USD | OutcomeToken,
456
457
  auto_withdraw: bool = True,
457
458
  api_keys: APIKeys | None = None,
@@ -460,8 +461,7 @@ class SeerAgentMarket(AgentMarket):
460
461
  """
461
462
  Sells the given number of shares for the given outcome in the given market.
462
463
  """
463
- outcome_str = self.get_outcome_str_from_bool(outcome)
464
- outcome_token = self.get_wrapped_token_for_outcome(outcome_str)
464
+ outcome_token = self.get_wrapped_token_for_outcome(outcome)
465
465
  api_keys = api_keys if api_keys is not None else APIKeys()
466
466
 
467
467
  token_amount = (