prediction-market-agent-tooling 0.49.1__py3-none-any.whl → 0.50.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 (43) hide show
  1. prediction_market_agent_tooling/benchmark/agents.py +6 -6
  2. prediction_market_agent_tooling/deploy/agent.py +5 -5
  3. prediction_market_agent_tooling/deploy/betting_strategy.py +83 -3
  4. prediction_market_agent_tooling/deploy/gcp/kubernetes_models.py +3 -4
  5. prediction_market_agent_tooling/gtypes.py +3 -2
  6. prediction_market_agent_tooling/jobs/jobs_models.py +3 -3
  7. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +2 -2
  8. prediction_market_agent_tooling/markets/agent_market.py +13 -17
  9. prediction_market_agent_tooling/markets/data_models.py +3 -3
  10. prediction_market_agent_tooling/markets/manifold/api.py +6 -6
  11. prediction_market_agent_tooling/markets/manifold/data_models.py +13 -23
  12. prediction_market_agent_tooling/markets/manifold/manifold.py +2 -2
  13. prediction_market_agent_tooling/markets/markets.py +7 -3
  14. prediction_market_agent_tooling/markets/metaculus/api.py +2 -3
  15. prediction_market_agent_tooling/markets/metaculus/data_models.py +11 -10
  16. prediction_market_agent_tooling/markets/metaculus/metaculus.py +2 -2
  17. prediction_market_agent_tooling/markets/omen/data_models.py +35 -22
  18. prediction_market_agent_tooling/markets/omen/omen.py +13 -9
  19. prediction_market_agent_tooling/markets/omen/omen_contracts.py +4 -2
  20. prediction_market_agent_tooling/markets/omen/omen_resolving.py +3 -4
  21. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +46 -33
  22. prediction_market_agent_tooling/markets/polymarket/api.py +1 -1
  23. prediction_market_agent_tooling/markets/polymarket/data_models.py +5 -6
  24. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +14 -14
  25. prediction_market_agent_tooling/markets/polymarket/polymarket.py +2 -2
  26. prediction_market_agent_tooling/monitor/markets/manifold.py +2 -2
  27. prediction_market_agent_tooling/monitor/markets/metaculus.py +2 -2
  28. prediction_market_agent_tooling/monitor/markets/omen.py +2 -2
  29. prediction_market_agent_tooling/monitor/markets/polymarket.py +2 -2
  30. prediction_market_agent_tooling/monitor/monitor.py +5 -15
  31. prediction_market_agent_tooling/monitor/monitor_app.py +7 -8
  32. prediction_market_agent_tooling/tools/contract.py +3 -7
  33. prediction_market_agent_tooling/tools/datetime_utc.py +74 -0
  34. prediction_market_agent_tooling/tools/httpx_cached_client.py +11 -0
  35. prediction_market_agent_tooling/tools/is_predictable.py +1 -1
  36. prediction_market_agent_tooling/tools/langfuse_client_utils.py +16 -11
  37. prediction_market_agent_tooling/tools/tavily_storage/tavily_models.py +5 -5
  38. prediction_market_agent_tooling/tools/utils.py +28 -52
  39. {prediction_market_agent_tooling-0.49.1.dist-info → prediction_market_agent_tooling-0.50.0.dist-info}/METADATA +5 -2
  40. {prediction_market_agent_tooling-0.49.1.dist-info → prediction_market_agent_tooling-0.50.0.dist-info}/RECORD +43 -41
  41. {prediction_market_agent_tooling-0.49.1.dist-info → prediction_market_agent_tooling-0.50.0.dist-info}/LICENSE +0 -0
  42. {prediction_market_agent_tooling-0.49.1.dist-info → prediction_market_agent_tooling-0.50.0.dist-info}/WHEEL +0 -0
  43. {prediction_market_agent_tooling-0.49.1.dist-info → prediction_market_agent_tooling-0.50.0.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  import random
2
2
  import typing as t
3
- from datetime import datetime
4
3
 
5
4
  from prediction_market_agent_tooling.benchmark.utils import (
6
5
  OutcomePrediction,
7
6
  Prediction,
8
7
  )
9
8
  from prediction_market_agent_tooling.gtypes import Probability
9
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC
10
10
 
11
11
 
12
12
  class AbstractBenchmarkedAgent:
@@ -41,7 +41,7 @@ class AbstractBenchmarkedAgent:
41
41
  def is_predictable_restricted(
42
42
  self,
43
43
  market_question: str,
44
- time_restriction_up_to: datetime,
44
+ time_restriction_up_to: DatetimeUTC,
45
45
  ) -> bool:
46
46
  """
47
47
  Override if the agent can decide to not predict the question, before doing the hard work.
@@ -53,7 +53,7 @@ class AbstractBenchmarkedAgent:
53
53
  def predict_restricted(
54
54
  self,
55
55
  market_question: str,
56
- time_restriction_up_to: datetime,
56
+ time_restriction_up_to: DatetimeUTC,
57
57
  ) -> Prediction:
58
58
  """
59
59
  Predict the outcome of the market question.
@@ -65,7 +65,7 @@ class AbstractBenchmarkedAgent:
65
65
  def check_and_predict_restricted(
66
66
  self,
67
67
  market_question: str,
68
- time_restriction_up_to: datetime,
68
+ time_restriction_up_to: DatetimeUTC,
69
69
  ) -> Prediction:
70
70
  """
71
71
  Data used must be restricted to the time_restriction_up_to.
@@ -94,7 +94,7 @@ class RandomAgent(AbstractBenchmarkedAgent):
94
94
  )
95
95
 
96
96
  def predict_restricted(
97
- self, market_question: str, time_restriction_up_to: datetime
97
+ self, market_question: str, time_restriction_up_to: DatetimeUTC
98
98
  ) -> Prediction:
99
99
  return self.predict(market_question)
100
100
 
@@ -117,6 +117,6 @@ class FixedAgent(AbstractBenchmarkedAgent):
117
117
  )
118
118
 
119
119
  def predict_restricted(
120
- self, market_question: str, time_restriction_up_to: datetime
120
+ self, market_question: str, time_restriction_up_to: DatetimeUTC
121
121
  ) -> Prediction:
122
122
  return self.predict(market_question)
@@ -4,7 +4,7 @@ import os
4
4
  import tempfile
5
5
  import time
6
6
  import typing as t
7
- from datetime import datetime, timedelta
7
+ from datetime import timedelta
8
8
  from enum import Enum
9
9
  from functools import cached_property
10
10
 
@@ -67,7 +67,7 @@ from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
67
67
  from prediction_market_agent_tooling.tools.ipfs.ipfs_handler import IPFSHandler
68
68
  from prediction_market_agent_tooling.tools.is_predictable import is_predictable_binary
69
69
  from prediction_market_agent_tooling.tools.langfuse_ import langfuse_context, observe
70
- from prediction_market_agent_tooling.tools.utils import DatetimeWithTimezone, utcnow
70
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC, utcnow
71
71
  from prediction_market_agent_tooling.tools.web3_utils import ipfscidv0_to_byte32
72
72
 
73
73
  MAX_AVAILABLE_MARKETS = 20
@@ -215,7 +215,7 @@ class DeployableAgent:
215
215
  secrets: dict[str, str] | None = None,
216
216
  cron_schedule: str | None = None,
217
217
  gcp_fname: str | None = None,
218
- start_time: DatetimeWithTimezone | None = None,
218
+ start_time: DatetimeUTC | None = None,
219
219
  timeout: int = 180,
220
220
  ) -> None:
221
221
  path_to_agent_file = os.path.relpath(inspect.getfile(self.__class__))
@@ -287,7 +287,7 @@ def {entrypoint_function_name}(request) -> str:
287
287
  raise NotImplementedError("This method must be implemented by the subclass.")
288
288
 
289
289
  def get_gcloud_fname(self, market_type: MarketType) -> str:
290
- return f"{self.__class__.__name__.lower()}-{market_type}-{datetime.now().strftime('%Y-%m-%d--%H-%M-%S')}"
290
+ return f"{self.__class__.__name__.lower()}-{market_type}-{utcnow().strftime('%Y-%m-%d--%H-%M-%S')}"
291
291
 
292
292
 
293
293
  class DeployableTraderAgent(DeployableAgent):
@@ -307,7 +307,7 @@ class DeployableTraderAgent(DeployableAgent):
307
307
  def get_betting_strategy(self, market: AgentMarket) -> BettingStrategy:
308
308
  user_id = market.get_user_id(api_keys=APIKeys())
309
309
 
310
- total_amount = market.get_user_balance(user_id=user_id) * 0.1
310
+ total_amount = market.get_tiny_bet_amount().amount
311
311
  if existing_position := market.get_position(user_id=user_id):
312
312
  total_amount += existing_position.total_amount.amount
313
313
 
@@ -1,5 +1,9 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
+ from scipy.optimize import minimize_scalar
4
+
5
+ from prediction_market_agent_tooling.gtypes import xDai
6
+ from prediction_market_agent_tooling.loggers import logger
3
7
  from prediction_market_agent_tooling.markets.agent_market import AgentMarket
4
8
  from prediction_market_agent_tooling.markets.data_models import (
5
9
  Currency,
@@ -10,10 +14,14 @@ from prediction_market_agent_tooling.markets.data_models import (
10
14
  TradeType,
11
15
  )
12
16
  from prediction_market_agent_tooling.markets.omen.data_models import get_boolean_outcome
17
+ from prediction_market_agent_tooling.markets.omen.omen import (
18
+ get_buy_outcome_token_amount,
19
+ )
13
20
  from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
14
21
  get_kelly_bet_full,
15
22
  get_kelly_bet_simplified,
16
23
  )
24
+ from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet
17
25
  from prediction_market_agent_tooling.tools.utils import check_not_none
18
26
 
19
27
 
@@ -134,8 +142,9 @@ class MaxExpectedValueBettingStrategy(MaxAccuracyBettingStrategy):
134
142
 
135
143
 
136
144
  class KellyBettingStrategy(BettingStrategy):
137
- def __init__(self, max_bet_amount: float):
145
+ def __init__(self, max_bet_amount: float, max_price_impact: float | None = None):
138
146
  self.max_bet_amount = max_bet_amount
147
+ self.max_price_impact = max_price_impact
139
148
 
140
149
  def calculate_trades(
141
150
  self,
@@ -165,9 +174,19 @@ class KellyBettingStrategy(BettingStrategy):
165
174
  )
166
175
  )
167
176
 
177
+ kelly_bet_size = kelly_bet.size
178
+ if self.max_price_impact:
179
+ # Adjust amount
180
+ max_price_impact_bet_amount = self.calculate_bet_amount_for_price_impact(
181
+ market, kelly_bet, 0
182
+ )
183
+
184
+ # We just don't want Kelly size to extrapolate price_impact - hence we take the min.
185
+ kelly_bet_size = min(kelly_bet.size, max_price_impact_bet_amount)
186
+
168
187
  amounts = {
169
188
  market.get_outcome_str_from_bool(kelly_bet.direction): TokenAmount(
170
- amount=kelly_bet.size, currency=market.currency
189
+ amount=kelly_bet_size, currency=market.currency
171
190
  ),
172
191
  }
173
192
  target_position = Position(market_id=market.id, amounts=amounts)
@@ -176,8 +195,69 @@ class KellyBettingStrategy(BettingStrategy):
176
195
  )
177
196
  return trades
178
197
 
198
+ def calculate_price_impact_for_bet_amount(
199
+ self, buy_direction: bool, bet_amount: float, yes: float, no: float, fee: float
200
+ ) -> float:
201
+ total_outcome_tokens = yes + no
202
+ expected_price = (
203
+ no / total_outcome_tokens if buy_direction else yes / total_outcome_tokens
204
+ )
205
+
206
+ tokens_to_buy = get_buy_outcome_token_amount(
207
+ bet_amount, buy_direction, yes, no, fee
208
+ )
209
+
210
+ actual_price = bet_amount / tokens_to_buy
211
+ # price_impact should always be > 0
212
+ price_impact = (actual_price - expected_price) / expected_price
213
+ return price_impact
214
+
215
+ def calculate_bet_amount_for_price_impact(
216
+ self,
217
+ market: AgentMarket,
218
+ kelly_bet: SimpleBet,
219
+ fee: float,
220
+ ) -> float:
221
+ def calculate_price_impact_deviation_from_target_price_impact(
222
+ bet_amount: xDai,
223
+ ) -> float:
224
+ price_impact = self.calculate_price_impact_for_bet_amount(
225
+ kelly_bet.direction,
226
+ bet_amount,
227
+ yes_outcome_pool_size,
228
+ no_outcome_pool_size,
229
+ fee,
230
+ )
231
+ # We return abs for the algorithm to converge to 0 instead of the min (and possibly negative) value.
232
+
233
+ max_price_impact = check_not_none(self.max_price_impact)
234
+ return abs(price_impact - max_price_impact)
235
+
236
+ if not market.outcome_token_pool:
237
+ logger.warning(
238
+ "Market outcome_token_pool is None, cannot calculate bet amount"
239
+ )
240
+ return kelly_bet.size
241
+
242
+ yes_outcome_pool_size = market.outcome_token_pool[
243
+ market.get_outcome_str_from_bool(True)
244
+ ]
245
+ no_outcome_pool_size = market.outcome_token_pool[
246
+ market.get_outcome_str_from_bool(False)
247
+ ]
248
+
249
+ # The bounds below have been found to work heuristically.
250
+ optimized_bet_amount = minimize_scalar(
251
+ calculate_price_impact_deviation_from_target_price_impact,
252
+ bounds=(0, 1000 * (yes_outcome_pool_size + no_outcome_pool_size)),
253
+ method="bounded",
254
+ tol=1e-11,
255
+ options={"maxiter": 10000},
256
+ )
257
+ return float(optimized_bet_amount.x)
258
+
179
259
  def __repr__(self) -> str:
180
- return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount})"
260
+ return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount}, max_price_impact={self.max_price_impact})"
181
261
 
182
262
 
183
263
  class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
@@ -1,11 +1,10 @@
1
- from datetime import datetime
2
1
  from typing import Any
3
2
 
4
3
  from pydantic import BaseModel
5
4
 
6
5
 
7
6
  class Metadata(BaseModel):
8
- creationTimestamp: datetime
7
+ creationTimestamp: int
9
8
  generation: int
10
9
  name: str
11
10
  namespace: str
@@ -15,12 +14,12 @@ class Metadata(BaseModel):
15
14
 
16
15
 
17
16
  class Metadata1(BaseModel):
18
- creationTimestamp: datetime | None
17
+ creationTimestamp: int | None
19
18
  name: str
20
19
 
21
20
 
22
21
  class Metadata2(BaseModel):
23
- creationTimestamp: datetime | None
22
+ creationTimestamp: int | None
24
23
  name: str
25
24
 
26
25
 
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
- from datetime import datetime
3
2
  from typing import NewType, Union
4
3
 
5
4
  from eth_typing.evm import ( # noqa: F401 # Import for the sake of easy importing with others from here.
@@ -17,6 +16,9 @@ from web3.types import ( # noqa: F401 # Import for the sake of easy importing
17
16
  Wei,
18
17
  )
19
18
 
19
+ from prediction_market_agent_tooling.tools.datetime_utc import ( # noqa: F401 # Import for the sake of easy importing with others from here.
20
+ DatetimeUTC,
21
+ )
20
22
  from prediction_market_agent_tooling.tools.hexbytes_custom import ( # noqa: F401 # Import for the sake of easy importing with others from here.
21
23
  HexBytes,
22
24
  )
@@ -32,7 +34,6 @@ OutcomeStr = NewType("OutcomeStr", str)
32
34
  Probability = NewType("Probability", float)
33
35
  Mana = NewType("Mana", float) # Manifold's "currency"
34
36
  USDC = NewType("USDC", float)
35
- DatetimeWithTimezone = NewType("DatetimeWithTimezone", datetime)
36
37
  ChainID = NewType("ChainID", int)
37
38
  IPFSCIDVersion0 = NewType("IPFSCIDVersion0", str)
38
39
 
@@ -1,6 +1,5 @@
1
1
  import typing as t
2
2
  from abc import ABC, abstractmethod
3
- from datetime import datetime
4
3
 
5
4
  from pydantic import BaseModel
6
5
 
@@ -9,6 +8,7 @@ from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
9
8
  FilterBy,
10
9
  SortBy,
11
10
  )
11
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC
12
12
 
13
13
 
14
14
  class SimpleJob(BaseModel):
@@ -16,7 +16,7 @@ class SimpleJob(BaseModel):
16
16
  job: str
17
17
  reward: float
18
18
  currency: str
19
- deadline: datetime
19
+ deadline: DatetimeUTC
20
20
 
21
21
 
22
22
  class JobAgentMarket(AgentMarket, ABC):
@@ -29,7 +29,7 @@ class JobAgentMarket(AgentMarket, ABC):
29
29
 
30
30
  @property
31
31
  @abstractmethod
32
- def deadline(self) -> datetime:
32
+ def deadline(self) -> DatetimeUTC:
33
33
  """Deadline for the job completion."""
34
34
 
35
35
  @abstractmethod
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
- from datetime import datetime
3
2
 
4
3
  from web3 import Web3
5
4
 
@@ -21,6 +20,7 @@ from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
21
20
  OmenSubgraphHandler,
22
21
  SortBy,
23
22
  )
23
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC
24
24
 
25
25
 
26
26
  class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
@@ -32,7 +32,7 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
32
32
  return self.question
33
33
 
34
34
  @property
35
- def deadline(self) -> datetime:
35
+ def deadline(self) -> DatetimeUTC:
36
36
  return self.close_time
37
37
 
38
38
  def get_reward(self, max_bond: float) -> float:
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
- from datetime import datetime
3
2
  from enum import Enum
4
3
 
5
4
  from eth_typing import ChecksumAddress
@@ -18,8 +17,8 @@ from prediction_market_agent_tooling.markets.data_models import (
18
17
  TokenAmount,
19
18
  )
20
19
  from prediction_market_agent_tooling.tools.utils import (
20
+ DatetimeUTC,
21
21
  check_not_none,
22
- convert_to_utc_datetime,
23
22
  should_not_happen,
24
23
  utcnow,
25
24
  )
@@ -28,6 +27,8 @@ from prediction_market_agent_tooling.tools.utils import (
28
27
  class SortBy(str, Enum):
29
28
  CLOSING_SOONEST = "closing-soonest"
30
29
  NEWEST = "newest"
30
+ HIGHEST_LIQUIDITY = "highest_liquidity"
31
+ LOWEST_LIQUIDITY = "lowest_liquidity"
31
32
  NONE = "none"
32
33
 
33
34
 
@@ -50,23 +51,16 @@ class AgentMarket(BaseModel):
50
51
  question: str
51
52
  description: str | None
52
53
  outcomes: list[str]
53
- outcome_token_pool: dict[
54
- str, float
55
- ] | None # Should be in currency of `currency` above.
54
+ outcome_token_pool: (
55
+ dict[str, float] | None
56
+ ) # Should be in currency of `currency` above.
56
57
  resolution: Resolution | None
57
- created_time: datetime | None
58
- close_time: datetime | None
58
+ created_time: DatetimeUTC | None
59
+ close_time: DatetimeUTC | None
59
60
  current_p_yes: Probability
60
61
  url: str
61
62
  volume: float | None # Should be in currency of `currency` above.
62
63
 
63
- _add_timezone_validator_created_time = field_validator("created_time")(
64
- convert_to_utc_datetime
65
- )
66
- _add_timezone_validator_close_time = field_validator("close_time")(
67
- convert_to_utc_datetime
68
- )
69
-
70
64
  @field_validator("outcome_token_pool")
71
65
  def validate_outcome_token_pool(
72
66
  cls,
@@ -180,7 +174,7 @@ class AgentMarket(BaseModel):
180
174
  limit: int,
181
175
  sort_by: SortBy,
182
176
  filter_by: FilterBy = FilterBy.OPEN,
183
- created_after: t.Optional[datetime] = None,
177
+ created_after: t.Optional[DatetimeUTC] = None,
184
178
  excluded_questions: set[str] | None = None,
185
179
  ) -> t.Sequence["AgentMarket"]:
186
180
  raise NotImplementedError("Subclasses must implement this method")
@@ -191,13 +185,15 @@ class AgentMarket(BaseModel):
191
185
 
192
186
  @staticmethod
193
187
  def get_bets_made_since(
194
- better_address: ChecksumAddress, start_time: datetime
188
+ better_address: ChecksumAddress, start_time: DatetimeUTC
195
189
  ) -> list[Bet]:
196
190
  raise NotImplementedError("Subclasses must implement this method")
197
191
 
198
192
  @staticmethod
199
193
  def get_resolved_bets_made_since(
200
- better_address: ChecksumAddress, start_time: datetime, end_time: datetime | None
194
+ better_address: ChecksumAddress,
195
+ start_time: DatetimeUTC,
196
+ end_time: DatetimeUTC | None,
201
197
  ) -> list[ResolvedBet]:
202
198
  raise NotImplementedError("Subclasses must implement this method")
203
199
 
@@ -1,10 +1,10 @@
1
- from datetime import datetime
2
1
  from enum import Enum
3
2
  from typing import Annotated, TypeAlias
4
3
 
5
4
  from pydantic import BaseModel, BeforeValidator, computed_field
6
5
 
7
6
  from prediction_market_agent_tooling.gtypes import OutcomeStr, Probability
7
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC
8
8
 
9
9
 
10
10
  class Currency(str, Enum):
@@ -40,7 +40,7 @@ class Bet(BaseModel):
40
40
  id: str
41
41
  amount: BetAmount
42
42
  outcome: bool
43
- created_time: datetime
43
+ created_time: DatetimeUTC
44
44
  market_question: str
45
45
  market_id: str
46
46
 
@@ -50,7 +50,7 @@ class Bet(BaseModel):
50
50
 
51
51
  class ResolvedBet(Bet):
52
52
  market_outcome: bool
53
- resolved_time: datetime
53
+ resolved_time: DatetimeUTC
54
54
  profit: ProfitAmount
55
55
 
56
56
  @computed_field # type: ignore[prop-decorator]
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
- from datetime import datetime
3
2
 
4
3
  import requests
5
4
  import tenacity
@@ -20,6 +19,7 @@ from prediction_market_agent_tooling.markets.manifold.data_models import (
20
19
  )
21
20
  from prediction_market_agent_tooling.tools.parallelism import par_map
22
21
  from prediction_market_agent_tooling.tools.utils import (
22
+ DatetimeUTC,
23
23
  response_list_to_model,
24
24
  response_to_model,
25
25
  )
@@ -47,7 +47,7 @@ def get_manifold_binary_markets(
47
47
  ]
48
48
  | None
49
49
  ) = "open",
50
- created_after: t.Optional[datetime] = None,
50
+ created_after: t.Optional[DatetimeUTC] = None,
51
51
  excluded_questions: set[str] | None = None,
52
52
  ) -> list[ManifoldMarket]:
53
53
  all_markets: list[ManifoldMarket] = []
@@ -167,8 +167,8 @@ def get_manifold_market(market_id: str) -> FullManifoldMarket:
167
167
  )
168
168
  def get_manifold_bets(
169
169
  user_id: str,
170
- start_time: datetime,
171
- end_time: t.Optional[datetime],
170
+ start_time: DatetimeUTC,
171
+ end_time: t.Optional[DatetimeUTC],
172
172
  ) -> list[ManifoldBet]:
173
173
  url = f"{MANIFOLD_API_BASE_URL}/v0/bets"
174
174
 
@@ -182,8 +182,8 @@ def get_manifold_bets(
182
182
 
183
183
  def get_resolved_manifold_bets(
184
184
  user_id: str,
185
- start_time: datetime,
186
- end_time: t.Optional[datetime],
185
+ start_time: DatetimeUTC,
186
+ end_time: t.Optional[DatetimeUTC],
187
187
  ) -> tuple[list[ManifoldBet], list[ManifoldMarket]]:
188
188
  bets = get_manifold_bets(user_id, start_time, end_time)
189
189
  markets: list[ManifoldMarket] = par_map(
@@ -1,8 +1,7 @@
1
1
  import typing as t
2
- from datetime import datetime, timedelta
3
2
  from enum import Enum
4
3
 
5
- from pydantic import BaseModel, field_validator
4
+ from pydantic import BaseModel
6
5
 
7
6
  from prediction_market_agent_tooling.gtypes import Mana, Probability
8
7
  from prediction_market_agent_tooling.markets.data_models import (
@@ -10,7 +9,7 @@ from prediction_market_agent_tooling.markets.data_models import (
10
9
  ProfitAmount,
11
10
  Resolution,
12
11
  )
13
- from prediction_market_agent_tooling.tools.utils import should_not_happen
12
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC, should_not_happen
14
13
 
15
14
  MANIFOLD_BASE_URL = "https://manifold.markets"
16
15
 
@@ -33,7 +32,7 @@ class ManifoldAnswersMode(str, Enum):
33
32
 
34
33
 
35
34
  class ManifoldAnswer(BaseModel):
36
- createdTime: datetime
35
+ createdTime: DatetimeUTC
37
36
  avatarUrl: str
38
37
  id: str
39
38
  username: str
@@ -55,17 +54,17 @@ class ManifoldMarket(BaseModel):
55
54
  id: str
56
55
  question: str
57
56
  creatorId: str
58
- closeTime: datetime
59
- createdTime: datetime
57
+ closeTime: DatetimeUTC
58
+ createdTime: DatetimeUTC
60
59
  creatorAvatarUrl: t.Optional[str] = None
61
60
  creatorName: str
62
61
  creatorUsername: str
63
62
  isResolved: bool
64
63
  resolution: t.Optional[Resolution] = None
65
- resolutionTime: t.Optional[datetime] = None
66
- lastBetTime: t.Optional[datetime] = None
67
- lastCommentTime: t.Optional[datetime] = None
68
- lastUpdatedTime: datetime
64
+ resolutionTime: t.Optional[DatetimeUTC] = None
65
+ lastBetTime: t.Optional[DatetimeUTC] = None
66
+ lastCommentTime: t.Optional[DatetimeUTC] = None
67
+ lastUpdatedTime: DatetimeUTC
69
68
  mechanism: str
70
69
  outcomeType: str
71
70
  p: t.Optional[float] = None
@@ -100,15 +99,6 @@ class ManifoldMarket(BaseModel):
100
99
  def __repr__(self) -> str:
101
100
  return f"Manifold's market: {self.question}"
102
101
 
103
- @field_validator("closeTime", mode="before")
104
- def clip_timestamp(cls, value: int) -> datetime:
105
- """
106
- Clip the timestamp to the maximum valid timestamp.
107
- """
108
- max_timestamp = (datetime.max - timedelta(days=1)).timestamp()
109
- value = int(min(value / 1000, max_timestamp))
110
- return datetime.fromtimestamp(value)
111
-
112
102
 
113
103
  class FullManifoldMarket(ManifoldMarket):
114
104
  # Some of these fields are available only in specific cases, see https://docs.manifold.markets/api#get-v0marketmarketid.
@@ -137,7 +127,7 @@ class ManifoldUser(BaseModel):
137
127
  """
138
128
 
139
129
  id: str
140
- createdTime: datetime
130
+ createdTime: DatetimeUTC
141
131
  name: str
142
132
  username: str
143
133
  url: str
@@ -154,7 +144,7 @@ class ManifoldUser(BaseModel):
154
144
  userDeleted: t.Optional[bool] = None
155
145
  balance: Mana
156
146
  totalDeposits: Mana
157
- lastBetTime: t.Optional[datetime] = None
147
+ lastBetTime: t.Optional[DatetimeUTC] = None
158
148
  currentBettingStreak: t.Optional[int] = None
159
149
  profitCached: ProfitCached
160
150
 
@@ -193,7 +183,7 @@ class ManifoldBet(BaseModel):
193
183
  loanAmount: Mana | None
194
184
  orderAmount: t.Optional[Mana] = None
195
185
  fills: t.Optional[list[ManifoldBetFills]] = None
196
- createdTime: datetime
186
+ createdTime: DatetimeUTC
197
187
  outcome: Resolution
198
188
 
199
189
  def get_resolved_boolean_outcome(self) -> bool:
@@ -237,4 +227,4 @@ class ManifoldContractMetric(BaseModel):
237
227
  userUsername: str
238
228
  userName: str
239
229
  userAvatarUrl: str
240
- lastBetTime: datetime
230
+ lastBetTime: DatetimeUTC
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
- from datetime import datetime
3
2
  from math import ceil
4
3
 
5
4
  from prediction_market_agent_tooling.config import APIKeys
@@ -23,6 +22,7 @@ from prediction_market_agent_tooling.markets.manifold.data_models import (
23
22
  from prediction_market_agent_tooling.tools.betting_strategies.minimum_bet_to_win import (
24
23
  minimum_bet_to_win,
25
24
  )
25
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC
26
26
 
27
27
 
28
28
  class ManifoldAgentMarket(AgentMarket):
@@ -83,7 +83,7 @@ class ManifoldAgentMarket(AgentMarket):
83
83
  limit: int,
84
84
  sort_by: SortBy,
85
85
  filter_by: FilterBy = FilterBy.OPEN,
86
- created_after: t.Optional[datetime] = None,
86
+ created_after: t.Optional[DatetimeUTC] = None,
87
87
  excluded_questions: set[str] | None = None,
88
88
  ) -> t.Sequence["ManifoldAgentMarket"]:
89
89
  sort: t.Literal["newest", "close-date"] | None
@@ -1,5 +1,5 @@
1
1
  import typing as t
2
- from datetime import datetime, timedelta
2
+ from datetime import timedelta
3
3
  from enum import Enum
4
4
 
5
5
  from prediction_market_agent_tooling.config import APIKeys
@@ -26,7 +26,11 @@ from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
26
26
  from prediction_market_agent_tooling.markets.polymarket.polymarket import (
27
27
  PolymarketAgentMarket,
28
28
  )
29
- from prediction_market_agent_tooling.tools.utils import should_not_happen, utcnow
29
+ from prediction_market_agent_tooling.tools.utils import (
30
+ DatetimeUTC,
31
+ should_not_happen,
32
+ utcnow,
33
+ )
30
34
 
31
35
 
32
36
  class MarketType(str, Enum):
@@ -57,7 +61,7 @@ def get_binary_markets(
57
61
  filter_by: FilterBy = FilterBy.OPEN,
58
62
  sort_by: SortBy = SortBy.NONE,
59
63
  excluded_questions: set[str] | None = None,
60
- created_after: datetime | None = None,
64
+ created_after: DatetimeUTC | None = None,
61
65
  ) -> t.Sequence[AgentMarket]:
62
66
  agent_market_class = MARKET_TYPE_TO_AGENT_MARKET[market_type]
63
67
  markets = agent_market_class.get_binary_markets(
@@ -1,4 +1,3 @@
1
- from datetime import datetime
2
1
  from typing import Union
3
2
 
4
3
  import requests
@@ -9,7 +8,7 @@ from prediction_market_agent_tooling.markets.metaculus.data_models import (
9
8
  MetaculusQuestion,
10
9
  MetaculusQuestions,
11
10
  )
12
- from prediction_market_agent_tooling.tools.utils import response_to_model
11
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC, response_to_model
13
12
 
14
13
  METACULUS_API_BASE_URL = "https://www.metaculus.com/api2"
15
14
 
@@ -65,7 +64,7 @@ def get_questions(
65
64
  order_by: str | None = None,
66
65
  offset: int = 0,
67
66
  tournament_id: int | None = None,
68
- created_after: datetime | None = None,
67
+ created_after: DatetimeUTC | None = None,
69
68
  status: str | None = None,
70
69
  ) -> list[MetaculusQuestion]:
71
70
  """