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
@@ -1,25 +1,33 @@
1
1
  import typing as t
2
2
  from enum import Enum
3
+ from math import prod
3
4
 
4
5
  from eth_typing import ChecksumAddress
5
6
  from pydantic import BaseModel, field_validator, model_validator
6
7
  from pydantic_core.core_schema import FieldValidationInfo
7
8
  from web3 import Web3
8
9
 
10
+ from prediction_market_agent_tooling.benchmark.utils import get_most_probable_outcome
9
11
  from prediction_market_agent_tooling.config import APIKeys
12
+ from prediction_market_agent_tooling.deploy.constants import (
13
+ INVALID_OUTCOME_LOWERCASE_IDENTIFIER,
14
+ NO_OUTCOME_LOWERCASE_IDENTIFIER,
15
+ YES_OUTCOME_LOWERCASE_IDENTIFIER,
16
+ )
10
17
  from prediction_market_agent_tooling.gtypes import (
11
- CollateralToken,
12
18
  OutcomeStr,
13
19
  OutcomeToken,
20
+ OutcomeWei,
14
21
  Probability,
15
22
  )
23
+ from prediction_market_agent_tooling.loggers import logger
16
24
  from prediction_market_agent_tooling.markets.data_models import (
17
25
  USD,
18
26
  Bet,
27
+ CategoricalProbabilisticAnswer,
19
28
  CollateralToken,
20
29
  ExistingPosition,
21
30
  PlacedTrade,
22
- ProbabilisticAnswer,
23
31
  Resolution,
24
32
  ResolvedBet,
25
33
  )
@@ -27,13 +35,12 @@ from prediction_market_agent_tooling.markets.market_fees import MarketFees
27
35
  from prediction_market_agent_tooling.tools.utils import (
28
36
  DatetimeUTC,
29
37
  check_not_none,
30
- should_not_happen,
31
38
  utcnow,
32
39
  )
33
40
 
34
41
 
35
42
  class ProcessedMarket(BaseModel):
36
- answer: ProbabilisticAnswer
43
+ answer: CategoricalProbabilisticAnswer
37
44
 
38
45
 
39
46
  class ProcessedTradedMarket(ProcessedMarket):
@@ -66,15 +73,32 @@ class AgentMarket(BaseModel):
66
73
  question: str
67
74
  description: str | None
68
75
  outcomes: t.Sequence[OutcomeStr]
69
- outcome_token_pool: dict[str, OutcomeToken] | None
76
+ outcome_token_pool: dict[OutcomeStr, OutcomeToken] | None
70
77
  resolution: Resolution | None
71
78
  created_time: DatetimeUTC | None
72
79
  close_time: DatetimeUTC | None
73
- current_p_yes: Probability
80
+
81
+ probabilities: dict[OutcomeStr, Probability]
74
82
  url: str
75
83
  volume: CollateralToken | None
76
84
  fees: MarketFees
77
85
 
86
+ @field_validator("probabilities")
87
+ def validate_probabilities(
88
+ cls,
89
+ probs: dict[OutcomeStr, Probability],
90
+ info: FieldValidationInfo,
91
+ ) -> dict[OutcomeStr, Probability]:
92
+ outcomes: t.Sequence[OutcomeStr] = check_not_none(info.data.get("outcomes"))
93
+ if set(probs.keys()) != set(outcomes):
94
+ raise ValueError("Keys of `probabilities` must match `outcomes` exactly.")
95
+ total = float(sum(probs.values()))
96
+ if not 0.999 <= total <= 1.001:
97
+ # We simply log a warning because for some use-cases (e.g. existing positions), the
98
+ # markets might be already closed hence no reliable outcome token prices exist anymore.
99
+ logger.warning(f"Probabilities for market {info.data=} do not sum to 1.")
100
+ return probs
101
+
78
102
  @field_validator("outcome_token_pool")
79
103
  def validate_outcome_token_pool(
80
104
  cls,
@@ -91,6 +115,14 @@ class AgentMarket(BaseModel):
91
115
  )
92
116
  return outcome_token_pool
93
117
 
118
+ def get_outcome_token_pool_by_outcome(self, outcome: OutcomeStr) -> OutcomeToken:
119
+ if self.outcome_token_pool is None or not self.outcome_token_pool:
120
+ return OutcomeToken(0)
121
+
122
+ # We look up by index to avoid having to deal with case sensitivity issues.
123
+ outcome_idx = self.get_outcome_index(outcome)
124
+ return list(self.outcome_token_pool.values())[outcome_idx]
125
+
94
126
  @model_validator(mode="before")
95
127
  def handle_legacy_fee(cls, data: dict[str, t.Any]) -> dict[str, t.Any]:
96
128
  # Backward compatibility for older `AgentMarket` without `fees`.
@@ -99,33 +131,51 @@ class AgentMarket(BaseModel):
99
131
  del data["fee"]
100
132
  return data
101
133
 
102
- @property
103
- def current_p_no(self) -> Probability:
104
- return Probability(1 - self.current_p_yes)
134
+ def market_outcome_for_probability_key(
135
+ self, probability_key: OutcomeStr
136
+ ) -> OutcomeStr:
137
+ for market_outcome in self.outcomes:
138
+ if market_outcome.lower() == probability_key.lower():
139
+ return market_outcome
140
+ raise ValueError(
141
+ f"Could not find probability for probability key {probability_key}"
142
+ )
143
+
144
+ def probability_for_market_outcome(self, market_outcome: OutcomeStr) -> Probability:
145
+ for k, v in self.probabilities.items():
146
+ if k.lower() == market_outcome.lower():
147
+ return v
148
+ raise ValueError(
149
+ f"Could not find probability for market outcome {market_outcome}"
150
+ )
105
151
 
106
152
  @property
107
- def yes_outcome_price(self) -> CollateralToken:
108
- """
109
- Price at prediction market is equal to the probability of given outcome.
110
- Keep as an extra property, in case it wouldn't be true for some prediction market platform.
111
- """
112
- return CollateralToken(self.current_p_yes)
153
+ def is_binary(self) -> bool:
154
+ # 3 outcomes can also be binary if 3rd outcome is invalid (Seer)
155
+ if len(self.outcomes) not in [2, 3]:
156
+ return False
113
157
 
114
- @property
115
- def yes_outcome_price_usd(self) -> USD:
116
- return self.get_token_in_usd(self.yes_outcome_price)
158
+ lowercase_outcomes = [outcome.lower() for outcome in self.outcomes]
159
+
160
+ has_yes = YES_OUTCOME_LOWERCASE_IDENTIFIER in lowercase_outcomes
161
+ has_no = NO_OUTCOME_LOWERCASE_IDENTIFIER in lowercase_outcomes
162
+
163
+ if len(lowercase_outcomes) == 3:
164
+ invalid_outcome = lowercase_outcomes[-1]
165
+ has_invalid = INVALID_OUTCOME_LOWERCASE_IDENTIFIER in invalid_outcome
166
+ return has_yes and has_no and has_invalid
167
+
168
+ return has_yes and has_no
117
169
 
118
170
  @property
119
- def no_outcome_price(self) -> CollateralToken:
120
- """
121
- Price at prediction market is equal to the probability of given outcome.
122
- Keep as an extra property, in case it wouldn't be true for some prediction market platform.
123
- """
124
- return CollateralToken(self.current_p_no)
171
+ def p_yes(self) -> Probability:
172
+ probs_lowercase = {o.lower(): p for o, p in self.probabilities.items()}
173
+ return check_not_none(probs_lowercase.get(YES_OUTCOME_LOWERCASE_IDENTIFIER))
125
174
 
126
175
  @property
127
- def no_outcome_price_usd(self) -> USD:
128
- return self.get_token_in_usd(self.no_outcome_price)
176
+ def p_no(self) -> Probability:
177
+ probs_lowercase = {o.lower(): p for o, p in self.probabilities.items()}
178
+ return check_not_none(probs_lowercase.get(NO_OUTCOME_LOWERCASE_IDENTIFIER))
129
179
 
130
180
  @property
131
181
  def probable_resolution(self) -> Resolution:
@@ -135,16 +185,8 @@ class AgentMarket(BaseModel):
135
185
  else:
136
186
  raise ValueError(f"Unknown resolution: {self.resolution}")
137
187
  else:
138
- return Resolution.YES if self.current_p_yes > 0.5 else Resolution.NO
139
-
140
- @property
141
- def boolean_outcome(self) -> bool:
142
- if self.resolution:
143
- if self.resolution == Resolution.YES:
144
- return True
145
- elif self.resolution == Resolution.NO:
146
- return False
147
- should_not_happen(f"Market {self.id} does not have a successful resolution.")
188
+ outcome = get_most_probable_outcome(self.probabilities)
189
+ return Resolution(outcome=outcome, invalid=False)
148
190
 
149
191
  def get_last_trade_p_yes(self) -> Probability | None:
150
192
  """
@@ -201,7 +243,7 @@ class AgentMarket(BaseModel):
201
243
  raise NotImplementedError("Subclasses must implement this method")
202
244
 
203
245
  def get_sell_value_of_outcome_token(
204
- self, outcome: str, amount: OutcomeToken
246
+ self, outcome: OutcomeStr, amount: OutcomeToken
205
247
  ) -> CollateralToken:
206
248
  """
207
249
  When you hold OutcomeToken(s), it's easy to calculate how much you get at the end if you win (1 OutcomeToken will equal to 1 Token).
@@ -225,30 +267,77 @@ class AgentMarket(BaseModel):
225
267
  """
226
268
  raise NotImplementedError("Subclasses must implement this method")
227
269
 
228
- def liquidate_existing_positions(self, outcome: bool) -> None:
270
+ def liquidate_existing_positions(self, outcome: OutcomeStr) -> None:
229
271
  raise NotImplementedError("Subclasses must implement this method")
230
272
 
231
- def place_bet(self, outcome: bool, amount: USD) -> str:
273
+ def place_bet(self, outcome: OutcomeStr, amount: USD) -> str:
232
274
  raise NotImplementedError("Subclasses must implement this method")
233
275
 
234
- def buy_tokens(self, outcome: bool, amount: USD) -> str:
276
+ def buy_tokens(self, outcome: OutcomeStr, amount: USD) -> str:
235
277
  return self.place_bet(outcome=outcome, amount=amount)
236
278
 
237
279
  def get_buy_token_amount(
238
- self, bet_amount: USD | CollateralToken, direction: bool
280
+ self, bet_amount: USD | CollateralToken, outcome: OutcomeStr
239
281
  ) -> OutcomeToken | None:
240
282
  raise NotImplementedError("Subclasses must implement this method")
241
283
 
242
- def sell_tokens(self, outcome: bool, amount: USD | OutcomeToken) -> str:
284
+ def sell_tokens(self, outcome: OutcomeStr, amount: USD | OutcomeToken) -> str:
243
285
  raise NotImplementedError("Subclasses must implement this method")
244
286
 
245
287
  @staticmethod
246
- def get_binary_markets(
288
+ def compute_fpmm_probabilities(balances: list[OutcomeWei]) -> list[Probability]:
289
+ """
290
+ Compute the implied probabilities in a Fixed Product Market Maker.
291
+
292
+ Args:
293
+ balances (List[float]): Balances of outcome tokens.
294
+
295
+ Returns:
296
+ List[float]: Implied probabilities for each outcome.
297
+ """
298
+ if all(x.value == 0 for x in balances):
299
+ return [Probability(0.0)] * len(balances)
300
+
301
+ # converting to standard values for prod compatibility.
302
+ values_balance = [i.value for i in balances]
303
+ # Compute product of balances excluding each outcome
304
+ excluded_products = []
305
+ for i in range(len(values_balance)):
306
+ other_balances = values_balance[:i] + values_balance[i + 1 :]
307
+ excluded_products.append(prod(other_balances))
308
+
309
+ # Normalize to sum to 1
310
+ total = sum(excluded_products)
311
+ if total == 0:
312
+ return [Probability(0.0)] * len(balances)
313
+ probabilities = [Probability(p / total) for p in excluded_products]
314
+
315
+ return probabilities
316
+
317
+ @staticmethod
318
+ def build_probability_map_from_p_yes(
319
+ p_yes: Probability,
320
+ ) -> dict[OutcomeStr, Probability]:
321
+ return {
322
+ OutcomeStr(YES_OUTCOME_LOWERCASE_IDENTIFIER): p_yes,
323
+ OutcomeStr(NO_OUTCOME_LOWERCASE_IDENTIFIER): Probability(1.0 - p_yes),
324
+ }
325
+
326
+ @staticmethod
327
+ def build_probability_map(
328
+ outcome_token_amounts: list[OutcomeWei], outcomes: list[OutcomeStr]
329
+ ) -> dict[OutcomeStr, Probability]:
330
+ probs = AgentMarket.compute_fpmm_probabilities(outcome_token_amounts)
331
+ return {outcome: prob for outcome, prob in zip(outcomes, probs)}
332
+
333
+ @staticmethod
334
+ def get_markets(
247
335
  limit: int,
248
336
  sort_by: SortBy,
249
337
  filter_by: FilterBy = FilterBy.OPEN,
250
338
  created_after: t.Optional[DatetimeUTC] = None,
251
339
  excluded_questions: set[str] | None = None,
340
+ fetch_categorical_markets: bool = False,
252
341
  ) -> t.Sequence["AgentMarket"]:
253
342
  raise NotImplementedError("Subclasses must implement this method")
254
343
 
@@ -328,12 +417,17 @@ class AgentMarket(BaseModel):
328
417
  return self.get_liquidity() > 0
329
418
 
330
419
  def has_successful_resolution(self) -> bool:
331
- return self.resolution in [Resolution.YES, Resolution.NO]
420
+ return (
421
+ self.resolution is not None
422
+ and self.resolution.outcome is not None
423
+ and not self.resolution.invalid
424
+ )
332
425
 
333
426
  def has_unsuccessful_resolution(self) -> bool:
334
- return self.resolution in [Resolution.CANCEL, Resolution.MKT]
427
+ return self.resolution is not None and self.resolution.invalid
335
428
 
336
- def get_outcome_str_from_bool(self, outcome: bool) -> OutcomeStr:
429
+ @staticmethod
430
+ def get_outcome_str_from_bool(outcome: bool) -> OutcomeStr:
337
431
  raise NotImplementedError("Subclasses must implement this method")
338
432
 
339
433
  def get_outcome_str(self, outcome_index: int) -> OutcomeStr:
@@ -344,13 +438,14 @@ class AgentMarket(BaseModel):
344
438
  f"Outcome index `{outcome_index}` out of range for `{self.outcomes}`: `{self.outcomes}`."
345
439
  )
346
440
 
347
- def get_outcome_index(self, outcome: str) -> int:
441
+ def get_outcome_index(self, outcome: OutcomeStr) -> int:
442
+ outcomes_lowercase = [o.lower() for o in self.outcomes]
348
443
  try:
349
- return self.outcomes.index(outcome)
444
+ return outcomes_lowercase.index(outcome.lower())
350
445
  except ValueError:
351
446
  raise ValueError(f"Outcome `{outcome}` not found in `{self.outcomes}`.")
352
447
 
353
- def get_token_balance(self, user_id: str, outcome: str) -> OutcomeToken:
448
+ def get_token_balance(self, user_id: str, outcome: OutcomeStr) -> OutcomeToken:
354
449
  raise NotImplementedError("Subclasses must implement this method")
355
450
 
356
451
  def get_position(self, user_id: str) -> ExistingPosition | None:
@@ -385,7 +480,7 @@ class AgentMarket(BaseModel):
385
480
  def has_token_pool(self) -> bool:
386
481
  return self.outcome_token_pool is not None
387
482
 
388
- def get_pool_tokens(self, outcome: str) -> OutcomeToken:
483
+ def get_pool_tokens(self, outcome: OutcomeStr) -> OutcomeToken:
389
484
  if not self.outcome_token_pool:
390
485
  raise ValueError("Outcome token pool is not available.")
391
486
 
@@ -16,6 +16,9 @@ from prediction_market_agent_tooling.tools.ipfs.ipfs_handler import IPFSHandler
16
16
  from prediction_market_agent_tooling.tools.utils import BPS_CONSTANT
17
17
  from prediction_market_agent_tooling.tools.web3_utils import ipfscidv0_to_byte32
18
18
 
19
+ # max uint16 for easy prediction identification (if market does not have YES outcome)
20
+ UINT16_MAX = 2**16 - 1 # = 65535
21
+
19
22
 
20
23
  def store_trades(
21
24
  market_id: str,
@@ -28,6 +31,10 @@ def store_trades(
28
31
  logger.warning(f"No prediction for market {market_id}, not storing anything.")
29
32
  return None
30
33
 
34
+ yes_probability = traded_market.answer.get_yes_probability()
35
+ if not yes_probability:
36
+ logger.info("Skipping this since no yes_probability available.")
37
+ return None
31
38
  reasoning = traded_market.answer.reasoning if traded_market.answer.reasoning else ""
32
39
 
33
40
  ipfs_hash_decoded = HexBytes(HASH_ZERO)
@@ -43,11 +50,13 @@ def store_trades(
43
50
  HexBytes(HexStr(i.id)) for i in traded_market.trades if i.id is not None
44
51
  ]
45
52
 
53
+ estimated_probability_bps = int(yes_probability * BPS_CONSTANT)
54
+
46
55
  prediction = ContractPrediction(
47
56
  publisher=keys.bet_from_address,
48
57
  ipfs_hash=ipfs_hash_decoded,
49
58
  tx_hashes=tx_hashes,
50
- estimated_probability_bps=int(traded_market.answer.p_yes * BPS_CONSTANT),
59
+ estimated_probability_bps=estimated_probability_bps,
51
60
  )
52
61
  tx_receipt = OmenAgentResultMappingContract().add_prediction(
53
62
  api_keys=keys,
@@ -3,6 +3,10 @@ from typing import Annotated
3
3
 
4
4
  from pydantic import BaseModel, BeforeValidator, computed_field
5
5
 
6
+ from prediction_market_agent_tooling.deploy.constants import (
7
+ NO_OUTCOME_LOWERCASE_IDENTIFIER,
8
+ YES_OUTCOME_LOWERCASE_IDENTIFIER,
9
+ )
6
10
  from prediction_market_agent_tooling.gtypes import (
7
11
  USD,
8
12
  CollateralToken,
@@ -11,24 +15,22 @@ from prediction_market_agent_tooling.gtypes import (
11
15
  Probability,
12
16
  )
13
17
  from prediction_market_agent_tooling.logprobs_parser import FieldLogprobs
14
- from prediction_market_agent_tooling.tools.utils import DatetimeUTC
18
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC, check_not_none
15
19
 
16
20
 
17
- class Resolution(str, Enum):
18
- YES = "YES"
19
- NO = "NO"
20
- CANCEL = "CANCEL"
21
- MKT = "MKT"
21
+ class Resolution(BaseModel):
22
+ outcome: OutcomeStr | None
23
+ invalid: bool
22
24
 
23
25
  @staticmethod
24
- def from_bool(value: bool) -> "Resolution":
25
- return Resolution.YES if value else Resolution.NO
26
+ def from_answer(answer: OutcomeStr) -> "Resolution":
27
+ return Resolution(outcome=answer, invalid=False)
26
28
 
27
29
 
28
30
  class Bet(BaseModel):
29
31
  id: str
30
32
  amount: CollateralToken
31
- outcome: bool
33
+ outcome: OutcomeStr
32
34
  created_time: DatetimeUTC
33
35
  market_question: str
34
36
  market_id: str
@@ -38,7 +40,7 @@ class Bet(BaseModel):
38
40
 
39
41
 
40
42
  class ResolvedBet(Bet):
41
- market_outcome: bool
43
+ market_outcome: OutcomeStr
42
44
  resolved_time: DatetimeUTC
43
45
  profit: CollateralToken
44
46
 
@@ -84,6 +86,72 @@ class ProbabilisticAnswer(BaseModel):
84
86
  def p_no(self) -> Probability:
85
87
  return Probability(1 - self.p_yes)
86
88
 
89
+ @property
90
+ def probable_resolution(self) -> Resolution:
91
+ return (
92
+ Resolution(
93
+ outcome=OutcomeStr(YES_OUTCOME_LOWERCASE_IDENTIFIER), invalid=False
94
+ )
95
+ if self.p_yes > 0.5
96
+ else Resolution(
97
+ outcome=OutcomeStr(NO_OUTCOME_LOWERCASE_IDENTIFIER), invalid=False
98
+ )
99
+ )
100
+
101
+
102
+ class CategoricalProbabilisticAnswer(BaseModel):
103
+ probabilities: dict[OutcomeStr, Probability]
104
+ confidence: float
105
+ reasoning: str | None = None
106
+
107
+ @property
108
+ def probable_resolution(self) -> Resolution:
109
+ most_likely_outcome = max(
110
+ self.probabilities.items(),
111
+ key=lambda item: item[1],
112
+ )[0]
113
+ return Resolution(outcome=most_likely_outcome, invalid=False)
114
+
115
+ def to_probabilistic_answer(self) -> ProbabilisticAnswer:
116
+ p_yes = check_not_none(self.get_yes_probability())
117
+ return ProbabilisticAnswer(
118
+ p_yes=p_yes,
119
+ confidence=self.confidence,
120
+ )
121
+
122
+ @staticmethod
123
+ def from_probabilistic_answer(
124
+ answer: ProbabilisticAnswer,
125
+ ) -> "CategoricalProbabilisticAnswer":
126
+ return CategoricalProbabilisticAnswer(
127
+ probabilities={
128
+ OutcomeStr(YES_OUTCOME_LOWERCASE_IDENTIFIER): answer.p_yes,
129
+ OutcomeStr(NO_OUTCOME_LOWERCASE_IDENTIFIER): Probability(
130
+ 1 - answer.p_yes
131
+ ),
132
+ },
133
+ confidence=answer.confidence,
134
+ reasoning=answer.reasoning,
135
+ )
136
+
137
+ def probability_for_market_outcome(self, market_outcome: OutcomeStr) -> Probability:
138
+ for k, v in self.probabilities.items():
139
+ if k.lower() == market_outcome.lower():
140
+ return v
141
+ raise ValueError(
142
+ f"Could not find probability for market outcome {market_outcome}"
143
+ )
144
+
145
+ def get_yes_probability(self) -> Probability | None:
146
+ return next(
147
+ (
148
+ p
149
+ for o, p in self.probabilities.items()
150
+ if o.lower() == YES_OUTCOME_LOWERCASE_IDENTIFIER
151
+ ),
152
+ None,
153
+ )
154
+
87
155
 
88
156
  class Position(BaseModel):
89
157
  market_id: str
@@ -124,7 +192,7 @@ class TradeType(str, Enum):
124
192
 
125
193
  class Trade(BaseModel):
126
194
  trade_type: TradeType
127
- outcome: bool
195
+ outcome: OutcomeStr
128
196
  amount: USD
129
197
 
130
198
 
@@ -144,13 +212,13 @@ class PlacedTrade(Trade):
144
212
  class SimulatedBetDetail(BaseModel):
145
213
  strategy: str
146
214
  url: str
147
- market_p_yes: float
148
- agent_p_yes: float
215
+ probabilities: dict[OutcomeStr, Probability]
216
+ agent_prob_multi: dict[OutcomeStr, Probability]
149
217
  agent_conf: float
150
218
  org_bet: CollateralToken
151
219
  sim_bet: CollateralToken
152
- org_dir: bool
153
- sim_dir: bool
220
+ org_dir: OutcomeStr
221
+ sim_dir: OutcomeStr
154
222
  org_profit: CollateralToken
155
223
  sim_profit: CollateralToken
156
224
  timestamp: DatetimeUTC
@@ -170,6 +238,4 @@ class SimulatedLifetimeDetail(BaseModel):
170
238
  total_simulated_profit: CollateralToken
171
239
  roi: float
172
240
  simulated_roi: float
173
- sharpe_output_original: SharpeOutput
174
- sharpe_output_simulation: SharpeOutput
175
241
  maximize: float
@@ -3,9 +3,9 @@ import typing as t
3
3
  import requests
4
4
  import tenacity
5
5
 
6
- from prediction_market_agent_tooling.gtypes import Mana, SecretStr
6
+ from prediction_market_agent_tooling.gtypes import Mana, OutcomeStr, SecretStr
7
7
  from prediction_market_agent_tooling.loggers import logger
8
- from prediction_market_agent_tooling.markets.data_models import ResolvedBet
8
+ from prediction_market_agent_tooling.markets.data_models import Resolution, ResolvedBet
9
9
  from prediction_market_agent_tooling.markets.manifold.data_models import (
10
10
  FullManifoldMarket,
11
11
  ManifoldBet,
@@ -105,14 +105,13 @@ def get_one_manifold_binary_market() -> ManifoldMarket:
105
105
  after=lambda x: logger.debug(f"place_bet failed, {x.attempt_number=}."),
106
106
  )
107
107
  def place_bet(
108
- amount: Mana, market_id: str, outcome: bool, manifold_api_key: SecretStr
108
+ amount: Mana, market_id: str, outcome: OutcomeStr, manifold_api_key: SecretStr
109
109
  ) -> ManifoldBet:
110
- outcome_str = "YES" if outcome else "NO"
111
110
  url = f"{MANIFOLD_API_BASE_URL}/v0/bet"
112
111
  params = {
113
112
  "amount": float(amount), # Convert to float to avoid serialization issues.
114
113
  "contractId": market_id,
115
- "outcome": outcome_str,
114
+ "outcome": outcome,
116
115
  }
117
116
 
118
117
  headers = {
@@ -204,11 +203,12 @@ def manifold_to_generic_resolved_bet(
204
203
  if not market.resolutionTime:
205
204
  raise ValueError(f"Market {market.id} has no resolution time.")
206
205
 
207
- market_outcome = market.get_resolved_boolean_outcome()
206
+ market_outcome = market.get_resolved_outcome()
207
+
208
208
  return ResolvedBet(
209
209
  id=bet.id,
210
210
  amount=bet.amount,
211
- outcome=bet.get_resolved_boolean_outcome(),
211
+ outcome=bet.get_resolved_outcome(),
212
212
  created_time=bet.createdTime,
213
213
  market_question=market.question,
214
214
  market_id=market.id,
@@ -224,3 +224,14 @@ def get_market_positions(market_id: str, user_id: str) -> list[ManifoldContractM
224
224
  return response_list_to_model(
225
225
  requests.get(url, params=params), ManifoldContractMetric
226
226
  )
227
+
228
+
229
+ def find_resolution_on_manifold(question: str, n: int = 100) -> Resolution | None:
230
+ # Even with exact-match search, Manifold doesn't return it as the first result, increase `n` if you can't find market that you know exists.
231
+ manifold_markets = get_manifold_binary_markets(
232
+ n, term=question, filter_=None, sort=None
233
+ )
234
+ for manifold_market in manifold_markets:
235
+ if manifold_market.question == question:
236
+ return manifold_market.resolution
237
+ return None
@@ -1,7 +1,7 @@
1
1
  import typing as t
2
2
  from enum import Enum
3
3
 
4
- from pydantic import BaseModel
4
+ from pydantic import BaseModel, field_validator
5
5
 
6
6
  from prediction_market_agent_tooling.gtypes import (
7
7
  USD,
@@ -12,6 +12,7 @@ from prediction_market_agent_tooling.gtypes import (
12
12
  Probability,
13
13
  )
14
14
  from prediction_market_agent_tooling.markets.data_models import Resolution
15
+ from prediction_market_agent_tooling.markets.manifold.utils import validate_resolution
15
16
  from prediction_market_agent_tooling.tools.utils import DatetimeUTC, should_not_happen
16
17
 
17
18
  MANIFOLD_BASE_URL = "https://manifold.markets"
@@ -92,21 +93,25 @@ class ManifoldMarket(BaseModel):
92
93
  def outcomes(self) -> t.Sequence[OutcomeStr]:
93
94
  return [OutcomeStr(o) for o in self.pool.model_fields.keys()]
94
95
 
95
- def get_resolved_boolean_outcome(self) -> bool:
96
- if self.resolution == Resolution.YES:
97
- return True
98
- elif self.resolution == Resolution.NO:
99
- return False
96
+ def get_resolved_outcome(self) -> OutcomeStr:
97
+ if self.resolution and self.resolution.outcome:
98
+ return self.resolution.outcome
100
99
  else:
101
- should_not_happen(f"Unexpected bet outcome string, '{self.resolution}'.")
100
+ raise ValueError(f"Market is not resolved. Resolution {self.resolution=}")
102
101
 
103
102
  def is_resolved_non_cancelled(self) -> bool:
104
103
  return (
105
104
  self.isResolved
106
105
  and self.resolutionTime is not None
107
- and self.resolution not in [Resolution.CANCEL, Resolution.MKT]
106
+ and self.resolution is not None
107
+ and self.resolution.outcome is not None
108
+ and not self.resolution.invalid
108
109
  )
109
110
 
111
+ @field_validator("resolution", mode="before")
112
+ def validate_resolution(cls, v: t.Any) -> Resolution:
113
+ return validate_resolution(v)
114
+
110
115
  def __repr__(self) -> str:
111
116
  return f"Manifold's market: {self.question}"
112
117
 
@@ -197,18 +202,20 @@ class ManifoldBet(BaseModel):
197
202
  createdTime: DatetimeUTC
198
203
  outcome: Resolution
199
204
 
200
- def get_resolved_boolean_outcome(self) -> bool:
201
- if self.outcome == Resolution.YES:
202
- return True
203
- elif self.outcome == Resolution.NO:
204
- return False
205
+ @field_validator("outcome", mode="before")
206
+ def validate_resolution(cls, v: t.Any) -> Resolution:
207
+ return validate_resolution(v)
208
+
209
+ def get_resolved_outcome(self) -> OutcomeStr:
210
+ if self.outcome.outcome:
211
+ return self.outcome.outcome
205
212
  else:
206
- should_not_happen(f"Unexpected bet outcome string, '{self.outcome.value}'.")
213
+ raise ValueError(f"Bet {self.id} is not resolved. {self.outcome=}")
207
214
 
208
- def get_profit(self, market_outcome: bool) -> CollateralToken:
215
+ def get_profit(self, market_outcome: OutcomeStr) -> CollateralToken:
209
216
  profit = (
210
217
  self.shares - self.amount
211
- if self.get_resolved_boolean_outcome() == market_outcome
218
+ if self.get_resolved_outcome() == market_outcome
212
219
  else -self.amount
213
220
  )
214
221
  return profit