prediction-market-agent-tooling 0.48.18__py3-none-any.whl → 0.49.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. prediction_market_agent_tooling/abis/debuggingcontract.abi.json +29 -0
  2. prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +171 -0
  3. prediction_market_agent_tooling/benchmark/benchmark.py +0 -93
  4. prediction_market_agent_tooling/config.py +16 -0
  5. prediction_market_agent_tooling/deploy/agent.py +86 -13
  6. prediction_market_agent_tooling/deploy/betting_strategy.py +5 -35
  7. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +2 -1
  8. prediction_market_agent_tooling/markets/agent_market.py +14 -6
  9. prediction_market_agent_tooling/markets/data_models.py +14 -0
  10. prediction_market_agent_tooling/markets/manifold/api.py +3 -1
  11. prediction_market_agent_tooling/markets/manifold/manifold.py +7 -2
  12. prediction_market_agent_tooling/markets/metaculus/metaculus.py +6 -1
  13. prediction_market_agent_tooling/markets/omen/data_models.py +247 -6
  14. prediction_market_agent_tooling/markets/omen/omen.py +77 -43
  15. prediction_market_agent_tooling/markets/omen/omen_contracts.py +179 -33
  16. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +35 -0
  17. prediction_market_agent_tooling/markets/polymarket/polymarket.py +1 -1
  18. prediction_market_agent_tooling/monitor/markets/polymarket.py +4 -0
  19. prediction_market_agent_tooling/monitor/monitor.py +3 -3
  20. prediction_market_agent_tooling/monitor/monitor_app.py +2 -2
  21. prediction_market_agent_tooling/tools/contract.py +50 -1
  22. prediction_market_agent_tooling/tools/ipfs/ipfs_handler.py +33 -0
  23. prediction_market_agent_tooling/tools/langfuse_client_utils.py +27 -12
  24. prediction_market_agent_tooling/tools/utils.py +28 -4
  25. prediction_market_agent_tooling/tools/web3_utils.py +7 -0
  26. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/METADATA +2 -1
  27. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/RECORD +30 -27
  28. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/LICENSE +0 -0
  29. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/WHEEL +0 -0
  30. {prediction_market_agent_tooling-0.48.18.dist-info → prediction_market_agent_tooling-0.49.1.dist-info}/entry_points.txt +0 -0
@@ -18,8 +18,8 @@ from prediction_market_agent_tooling.markets.data_models import (
18
18
  TokenAmount,
19
19
  )
20
20
  from prediction_market_agent_tooling.tools.utils import (
21
- add_utc_timezone_validator,
22
21
  check_not_none,
22
+ convert_to_utc_datetime,
23
23
  should_not_happen,
24
24
  utcnow,
25
25
  )
@@ -61,10 +61,10 @@ class AgentMarket(BaseModel):
61
61
  volume: float | None # Should be in currency of `currency` above.
62
62
 
63
63
  _add_timezone_validator_created_time = field_validator("created_time")(
64
- add_utc_timezone_validator
64
+ convert_to_utc_datetime
65
65
  )
66
66
  _add_timezone_validator_close_time = field_validator("close_time")(
67
- add_utc_timezone_validator
67
+ convert_to_utc_datetime
68
68
  )
69
69
 
70
70
  @field_validator("outcome_token_pool")
@@ -166,13 +166,13 @@ class AgentMarket(BaseModel):
166
166
  def liquidate_existing_positions(self, outcome: bool) -> None:
167
167
  raise NotImplementedError("Subclasses must implement this method")
168
168
 
169
- def place_bet(self, outcome: bool, amount: BetAmount) -> None:
169
+ def place_bet(self, outcome: bool, amount: BetAmount) -> str:
170
170
  raise NotImplementedError("Subclasses must implement this method")
171
171
 
172
- def buy_tokens(self, outcome: bool, amount: TokenAmount) -> None:
172
+ def buy_tokens(self, outcome: bool, amount: TokenAmount) -> str:
173
173
  return self.place_bet(outcome=outcome, amount=amount)
174
174
 
175
- def sell_tokens(self, outcome: bool, amount: TokenAmount) -> None:
175
+ def sell_tokens(self, outcome: bool, amount: TokenAmount) -> str:
176
176
  raise NotImplementedError("Subclasses must implement this method")
177
177
 
178
178
  @staticmethod
@@ -281,3 +281,11 @@ class AgentMarket(BaseModel):
281
281
  raise ValueError("Outcome token pool is not available.")
282
282
 
283
283
  return self.outcome_token_pool[outcome]
284
+
285
+ @staticmethod
286
+ def get_user_balance(user_id: str) -> float:
287
+ raise NotImplementedError("Subclasses must implement this method")
288
+
289
+ @staticmethod
290
+ def get_user_id(api_keys: APIKeys) -> str:
291
+ raise NotImplementedError("Subclasses must implement this method")
@@ -37,6 +37,7 @@ ProfitAmount: TypeAlias = TokenAmount
37
37
 
38
38
 
39
39
  class Bet(BaseModel):
40
+ id: str
40
41
  amount: BetAmount
41
42
  outcome: bool
42
43
  created_time: datetime
@@ -126,3 +127,16 @@ class Trade(BaseModel):
126
127
  trade_type: TradeType
127
128
  outcome: bool
128
129
  amount: TokenAmount
130
+
131
+
132
+ class PlacedTrade(Trade):
133
+ id: str | None = None
134
+
135
+ @staticmethod
136
+ def from_trade(trade: Trade, id: str) -> "PlacedTrade":
137
+ return PlacedTrade(
138
+ trade_type=trade.trade_type,
139
+ outcome=trade.outcome,
140
+ amount=trade.amount,
141
+ id=id,
142
+ )
@@ -110,7 +110,7 @@ def get_one_manifold_binary_market() -> ManifoldMarket:
110
110
  )
111
111
  def place_bet(
112
112
  amount: Mana, market_id: str, outcome: bool, manifold_api_key: SecretStr
113
- ) -> None:
113
+ ) -> ManifoldBet:
114
114
  outcome_str = "YES" if outcome else "NO"
115
115
  url = f"{MANIFOLD_API_BASE_URL}/v0/bet"
116
116
  params = {
@@ -131,6 +131,7 @@ def place_bet(
131
131
  raise RuntimeError(
132
132
  f"Placing bet failed: {response.status_code} {response.reason} {response.text}"
133
133
  )
134
+ return ManifoldBet.model_validate(data)
134
135
  else:
135
136
  raise Exception(
136
137
  f"Placing bet failed: {response.status_code} {response.reason} {response.text}"
@@ -209,6 +210,7 @@ def manifold_to_generic_resolved_bet(
209
210
 
210
211
  market_outcome = market.get_resolved_boolean_outcome()
211
212
  return ResolvedBet(
213
+ id=bet.id,
212
214
  amount=BetAmount(amount=bet.amount, currency=Currency.Mana),
213
215
  outcome=bet.get_resolved_boolean_outcome(),
214
216
  created_time=bet.createdTime,
@@ -49,15 +49,16 @@ class ManifoldAgentMarket(AgentMarket):
49
49
  # Manifold lowest bet is 1 Mana, so we need to ceil the result.
50
50
  return mana_type(ceil(minimum_bet_to_win(answer, amount_to_win, self)))
51
51
 
52
- def place_bet(self, outcome: bool, amount: BetAmount) -> None:
52
+ def place_bet(self, outcome: bool, amount: BetAmount) -> str:
53
53
  if amount.currency != self.currency:
54
54
  raise ValueError(f"Manifold bets are made in Mana. Got {amount.currency}.")
55
- place_bet(
55
+ bet = place_bet(
56
56
  amount=Mana(amount.amount),
57
57
  market_id=self.id,
58
58
  outcome=outcome,
59
59
  manifold_api_key=APIKeys().manifold_api_key,
60
60
  )
61
+ return bet.id
61
62
 
62
63
  @staticmethod
63
64
  def from_data_model(model: FullManifoldMarket) -> "ManifoldAgentMarket":
@@ -119,3 +120,7 @@ class ManifoldAgentMarket(AgentMarket):
119
120
  @classmethod
120
121
  def get_user_url(cls, keys: APIKeys) -> str:
121
122
  return get_authenticated_user(keys.manifold_api_key.get_secret_value()).url
123
+
124
+ @staticmethod
125
+ def get_user_id(api_keys: APIKeys) -> str:
126
+ return api_keys.manifold_user_id
@@ -1,6 +1,7 @@
1
1
  import typing as t
2
2
  from datetime import datetime
3
3
 
4
+ from prediction_market_agent_tooling.config import APIKeys
4
5
  from prediction_market_agent_tooling.gtypes import Probability
5
6
  from prediction_market_agent_tooling.markets.agent_market import (
6
7
  AgentMarket,
@@ -99,8 +100,12 @@ class MetaculusAgentMarket(AgentMarket):
99
100
 
100
101
  if len(all_questions) >= limit:
101
102
  break
102
- return [MetaculusAgentMarket.from_data_model(q) for q in all_questions]
103
+ return [MetaculusAgentMarket.from_data_model(q) for q in all_questions[:limit]]
103
104
 
104
105
  def submit_prediction(self, p_yes: Probability, reasoning: str) -> None:
105
106
  make_prediction(self.id, p_yes)
106
107
  post_question_comment(self.id, reasoning)
108
+
109
+ @staticmethod
110
+ def get_user_id(api_keys: APIKeys) -> str:
111
+ return str(api_keys.metaculus_user_id)
@@ -1,7 +1,8 @@
1
1
  import typing as t
2
2
  from datetime import datetime
3
3
 
4
- from pydantic import BaseModel
4
+ import pytz
5
+ from pydantic import BaseModel, ConfigDict, Field, computed_field
5
6
  from web3 import Web3
6
7
 
7
8
  from prediction_market_agent_tooling.gtypes import (
@@ -13,6 +14,7 @@ from prediction_market_agent_tooling.gtypes import (
13
14
  OmenOutcomeToken,
14
15
  Probability,
15
16
  Wei,
17
+ wei_type,
16
18
  xDai,
17
19
  )
18
20
  from prediction_market_agent_tooling.markets.data_models import (
@@ -37,6 +39,7 @@ INVALID_ANSWER_HEX_BYTES = HexBytes(INVALID_ANSWER)
37
39
  INVALID_ANSWER_STR = HexStr(INVALID_ANSWER_HEX_BYTES.hex())
38
40
  OMEN_BASE_URL = "https://aiomen.eth.limo"
39
41
  PRESAGIO_BASE_URL = "https://presagio.pages.dev"
42
+ TEST_CATEGORY = "test" # This category is hidden on Presagio for testing purposes.
40
43
 
41
44
 
42
45
  def get_boolean_outcome(outcome_str: str) -> bool:
@@ -207,8 +210,6 @@ class OmenMarket(BaseModel):
207
210
  creationTimestamp: int
208
211
  condition: Condition
209
212
  question: Question
210
- lastActiveDay: int
211
- lastActiveHour: int
212
213
 
213
214
  @property
214
215
  def openingTimestamp(self) -> int:
@@ -218,7 +219,7 @@ class OmenMarket(BaseModel):
218
219
 
219
220
  @property
220
221
  def opening_datetime(self) -> datetime:
221
- return datetime.fromtimestamp(self.openingTimestamp)
222
+ return datetime.fromtimestamp(self.openingTimestamp, tz=pytz.UTC)
222
223
 
223
224
  @property
224
225
  def close_time(self) -> datetime:
@@ -376,13 +377,103 @@ class OmenMarket(BaseModel):
376
377
  def url(self) -> str:
377
378
  return f"{PRESAGIO_BASE_URL}/markets?id={self.id}"
378
379
 
380
+ @staticmethod
381
+ def from_created_market(model: "CreatedMarket") -> "OmenMarket":
382
+ """
383
+ OmenMarket is meant to be retrieved from subgraph, however in tests against local chain it's very handy to create it out of `CreatedMarket`,
384
+ which is collection of events that are emitted during the market creation in omen_create_market_tx function.
385
+ """
386
+ if len(model.market_event.conditionIds) != 1:
387
+ raise ValueError(
388
+ f"Unexpected number of conditions: {len(model.market_event.conditionIds)}"
389
+ )
390
+ outcome_token_amounts = model.funding_event.outcome_token_amounts
391
+ return OmenMarket(
392
+ id=HexAddress(
393
+ HexStr(model.market_event.fixedProductMarketMaker.lower())
394
+ ), # Lowering to be identical with subgraph's output.
395
+ title=model.question_event.parsed_question.question,
396
+ creator=HexAddress(
397
+ HexStr(model.market_event.creator.lower())
398
+ ), # Lowering to be identical with subgraph's output.
399
+ category=model.question_event.parsed_question.category,
400
+ collateralVolume=Wei(0), # No volume possible yet.
401
+ liquidityParameter=calculate_liquidity_parameter(outcome_token_amounts),
402
+ usdVolume=USD(0), # No volume possible yet.
403
+ fee=model.fee,
404
+ collateralToken=HexAddress(
405
+ HexStr(model.market_event.collateralToken.lower())
406
+ ), # Lowering to be identical with subgraph's output.
407
+ outcomes=model.question_event.parsed_question.outcomes,
408
+ outcomeTokenAmounts=outcome_token_amounts,
409
+ outcomeTokenMarginalPrices=calculate_marginal_prices(outcome_token_amounts),
410
+ answerFinalizedTimestamp=None, # It's a fresh market.
411
+ currentAnswer=None, # It's a fresh market.
412
+ creationTimestamp=model.market_creation_timestamp,
413
+ condition=Condition(
414
+ id=model.market_event.conditionIds[0],
415
+ outcomeSlotCount=len(model.question_event.parsed_question.outcomes),
416
+ ),
417
+ question=Question(
418
+ id=model.question_event.question_id,
419
+ title=model.question_event.parsed_question.question,
420
+ data=model.question_event.question, # Question in the event holds the "raw" data.
421
+ templateId=model.question_event.template_id,
422
+ outcomes=model.question_event.parsed_question.outcomes,
423
+ isPendingArbitration=False, # Can not be, it's a fresh market.
424
+ openingTimestamp=model.question_event.opening_ts,
425
+ answerFinalizedTimestamp=None, # It's a new one, can not be.
426
+ currentAnswer=None, # It's a new one, no answer yet.
427
+ ),
428
+ )
429
+
430
+
431
+ def calculate_liquidity_parameter(
432
+ outcome_token_amounts: list[OmenOutcomeToken],
433
+ ) -> Wei:
434
+ """
435
+ Converted to Python from https://github.com/protofire/omen-subgraph/blob/f92bbfb6fa31ed9cd5985c416a26a2f640837d8b/src/utils/fpmm.ts#L171.
436
+ """
437
+ amounts_product = 1.0
438
+ for amount in outcome_token_amounts:
439
+ amounts_product *= amount
440
+ n = len(outcome_token_amounts)
441
+ liquidity_parameter = amounts_product ** (1.0 / n)
442
+ return wei_type(liquidity_parameter)
443
+
444
+
445
+ def calculate_marginal_prices(
446
+ outcome_token_amounts: list[OmenOutcomeToken],
447
+ ) -> list[xDai] | None:
448
+ """
449
+ Converted to Python from https://github.com/protofire/omen-subgraph/blob/f92bbfb6fa31ed9cd5985c416a26a2f640837d8b/src/utils/fpmm.ts#L197.
450
+ """
451
+ all_non_zero = all(x != 0 for x in outcome_token_amounts)
452
+ if not all_non_zero:
453
+ return None
454
+
455
+ n_outcomes = len(outcome_token_amounts)
456
+ weights = []
457
+
458
+ for i in range(n_outcomes):
459
+ weight = 1.0
460
+ for j in range(n_outcomes):
461
+ if i != j:
462
+ weight *= outcome_token_amounts[j]
463
+ weights.append(weight)
464
+
465
+ sum_weights = sum(weights)
466
+
467
+ marginal_prices = [weights[i] / sum_weights for i in range(n_outcomes)]
468
+ return [xDai(mp) for mp in marginal_prices]
469
+
379
470
 
380
471
  class OmenBetCreator(BaseModel):
381
472
  id: HexAddress
382
473
 
383
474
 
384
475
  class OmenBet(BaseModel):
385
- id: HexAddress
476
+ id: HexAddress # A concatenation of: FPMM contract ID, trader ID and nonce. See https://github.com/protofire/omen-subgraph/blob/f92bbfb6fa31ed9cd5985c416a26a2f640837d8b/src/FixedProductMarketMakerMapping.ts#L109
386
477
  title: str
387
478
  collateralToken: HexAddress
388
479
  outcomeTokenMarginalPrice: xDai
@@ -400,7 +491,7 @@ class OmenBet(BaseModel):
400
491
 
401
492
  @property
402
493
  def creation_datetime(self) -> datetime:
403
- return datetime.fromtimestamp(self.creationTimestamp)
494
+ return datetime.fromtimestamp(self.creationTimestamp, tz=pytz.UTC)
404
495
 
405
496
  @property
406
497
  def boolean_outcome(self) -> bool:
@@ -431,6 +522,9 @@ class OmenBet(BaseModel):
431
522
 
432
523
  def to_bet(self) -> Bet:
433
524
  return Bet(
525
+ id=str(
526
+ self.transactionHash
527
+ ), # Use the transaction hash instead of the bet id - both are valid, but we return the transaction hash from the trade functions, so be consistent here.
434
528
  amount=BetAmount(amount=self.collateralAmountUSD, currency=Currency.xDai),
435
529
  outcome=self.boolean_outcome,
436
530
  created_time=self.creation_datetime,
@@ -445,6 +539,9 @@ class OmenBet(BaseModel):
445
539
  )
446
540
 
447
541
  return ResolvedBet(
542
+ id=str(
543
+ self.transactionHash
544
+ ), # Use the transaction hash instead of the bet id - both are valid, but we return the transaction hash from the trade functions, so be consistent here.
448
545
  amount=BetAmount(amount=self.collateralAmountUSD, currency=Currency.xDai),
449
546
  outcome=self.boolean_outcome,
450
547
  created_time=self.creation_datetime,
@@ -527,3 +624,147 @@ class RealityAnswers(BaseModel):
527
624
 
528
625
  class RealityAnswersResponse(BaseModel):
529
626
  data: RealityAnswers
627
+
628
+
629
+ def format_realitio_question(
630
+ question: str,
631
+ outcomes: list[str],
632
+ category: str,
633
+ language: str,
634
+ template_id: int,
635
+ ) -> str:
636
+ """If you add a new template id here, also add to the parsing function below."""
637
+ if template_id == 2:
638
+ return "␟".join(
639
+ [
640
+ question,
641
+ ",".join(f'"{o}"' for o in outcomes),
642
+ category,
643
+ language,
644
+ ]
645
+ )
646
+
647
+ raise ValueError(f"Unsupported template id {template_id}.")
648
+
649
+
650
+ def parse_realitio_question(question_raw: str, template_id: int) -> "ParsedQuestion":
651
+ """If you add a new template id here, also add to the encoding function above."""
652
+ if template_id == 2:
653
+ question, outcomes_raw, category, language = question_raw.split("␟")
654
+ outcomes = [o.strip('"') for o in outcomes_raw.split(",")]
655
+ return ParsedQuestion(
656
+ question=question, outcomes=outcomes, category=category, language=language
657
+ )
658
+
659
+ raise ValueError(f"Unsupported template id {template_id}.")
660
+
661
+
662
+ class ParsedQuestion(BaseModel):
663
+ question: str
664
+ outcomes: list[str]
665
+ language: str
666
+ category: str
667
+
668
+
669
+ class RealitioLogNewQuestionEvent(BaseModel):
670
+ question_id: HexBytes
671
+ user: HexAddress
672
+ template_id: int
673
+ question: str # Be aware, this is question in format of format_realitio_question function, it's raw data.
674
+ content_hash: HexBytes
675
+ arbitrator: HexAddress
676
+ timeout: int
677
+ opening_ts: int
678
+ nonce: int
679
+ created: int
680
+
681
+ @property
682
+ def user_checksummed(self) -> ChecksumAddress:
683
+ return Web3.to_checksum_address(self.user)
684
+
685
+ @property
686
+ def parsed_question(self) -> ParsedQuestion:
687
+ return parse_realitio_question(
688
+ question_raw=self.question, template_id=self.template_id
689
+ )
690
+
691
+
692
+ class OmenFixedProductMarketMakerCreationEvent(BaseModel):
693
+ creator: HexAddress
694
+ fixedProductMarketMaker: HexAddress
695
+ conditionalTokens: HexAddress
696
+ collateralToken: HexAddress
697
+ conditionIds: list[HexBytes]
698
+ fee: int
699
+
700
+ @property
701
+ def creator_checksummed(self) -> ChecksumAddress:
702
+ return Web3.to_checksum_address(self.creator)
703
+
704
+ @property
705
+ def fixed_product_market_maker_checksummed(self) -> ChecksumAddress:
706
+ return Web3.to_checksum_address(self.fixedProductMarketMaker)
707
+
708
+ @property
709
+ def conditional_tokens_checksummed(self) -> ChecksumAddress:
710
+ return Web3.to_checksum_address(self.conditionalTokens)
711
+
712
+ @property
713
+ def collateral_token_checksummed(self) -> ChecksumAddress:
714
+ return Web3.to_checksum_address(self.collateralToken)
715
+
716
+
717
+ class ConditionPreparationEvent(BaseModel):
718
+ conditionId: HexBytes
719
+ oracle: HexAddress
720
+ questionId: HexBytes
721
+ outcomeSlotCount: int
722
+
723
+
724
+ class FPMMFundingAddedEvent(BaseModel):
725
+ funder: HexAddress
726
+ amountsAdded: list[OmenOutcomeToken]
727
+ sharesMinted: Wei
728
+
729
+ @property
730
+ def outcome_token_amounts(self) -> list[OmenOutcomeToken]:
731
+ # Just renaming so we remember what it is.
732
+ return self.amountsAdded
733
+
734
+
735
+ class CreatedMarket(BaseModel):
736
+ market_creation_timestamp: int
737
+ market_event: OmenFixedProductMarketMakerCreationEvent
738
+ funding_event: FPMMFundingAddedEvent
739
+ condition_id: HexBytes
740
+ question_event: RealitioLogNewQuestionEvent
741
+ condition_event: ConditionPreparationEvent | None
742
+ initial_funds: Wei
743
+ fee: Wei
744
+ distribution_hint: list[OmenOutcomeToken] | None
745
+
746
+
747
+ class ContractPrediction(BaseModel):
748
+ model_config = ConfigDict(populate_by_name=True)
749
+ publisher: str = Field(..., alias="publisherAddress")
750
+ ipfs_hash: HexBytes = Field(..., alias="ipfsHash")
751
+ tx_hashes: list[HexBytes] = Field(..., alias="txHashes")
752
+ estimated_probability_bps: int = Field(..., alias="estimatedProbabilityBps")
753
+
754
+ @computed_field # type: ignore[prop-decorator] # Mypy issue: https://github.com/python/mypy/issues/14461
755
+ @property
756
+ def publisher_checksummed(self) -> ChecksumAddress:
757
+ return Web3.to_checksum_address(self.publisher)
758
+
759
+ @staticmethod
760
+ def from_tuple(values: tuple[t.Any]) -> "ContractPrediction":
761
+ data = {k: v for k, v in zip(ContractPrediction.model_fields.keys(), values)}
762
+ return ContractPrediction.model_validate(data)
763
+
764
+
765
+ class IPFSAgentResult(BaseModel):
766
+ reasoning: str
767
+
768
+ model_config = ConfigDict(
769
+ extra="forbid",
770
+ )