prediction-market-agent-tooling 0.67.3__py3-none-any.whl → 0.67.4.dev992__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 (24) hide show
  1. prediction_market_agent_tooling/deploy/agent.py +2 -4
  2. prediction_market_agent_tooling/deploy/betting_strategy.py +169 -49
  3. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +2 -2
  4. prediction_market_agent_tooling/markets/agent_market.py +17 -0
  5. prediction_market_agent_tooling/markets/blockchain_utils.py +5 -3
  6. prediction_market_agent_tooling/markets/data_models.py +23 -3
  7. prediction_market_agent_tooling/markets/omen/data_models.py +6 -1
  8. prediction_market_agent_tooling/markets/omen/omen.py +43 -6
  9. prediction_market_agent_tooling/markets/polymarket/api.py +9 -3
  10. prediction_market_agent_tooling/markets/polymarket/data_models.py +5 -3
  11. prediction_market_agent_tooling/markets/polymarket/polymarket.py +15 -7
  12. prediction_market_agent_tooling/markets/seer/seer.py +11 -2
  13. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +4 -1
  14. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +276 -8
  15. prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
  16. prediction_market_agent_tooling/tools/hexbytes_custom.py +9 -0
  17. prediction_market_agent_tooling/tools/httpx_cached_client.py +5 -3
  18. prediction_market_agent_tooling/tools/langfuse_client_utils.py +4 -3
  19. prediction_market_agent_tooling/tools/singleton.py +11 -6
  20. {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/METADATA +1 -1
  21. {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/RECORD +24 -24
  22. {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/LICENSE +0 -0
  23. {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/WHEEL +0 -0
  24. {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/entry_points.txt +0 -0
@@ -59,16 +59,18 @@ class PolymarketGammaResponseDataItem(BaseModel):
59
59
  id: str
60
60
  slug: str
61
61
  volume: float | None = None
62
- startDate: DatetimeUTC
62
+ startDate: DatetimeUTC | None = None
63
63
  endDate: DatetimeUTC | None = None
64
64
  liquidity: float | None = None
65
65
  liquidityClob: float | None = None
66
66
  title: str
67
- description: str
67
+ description: str | None = None
68
68
  archived: bool
69
69
  closed: bool
70
70
  active: bool
71
- markets: list[PolymarketGammaMarket]
71
+ markets: list[
72
+ PolymarketGammaMarket
73
+ ] | None = None # Some Polymarket markets have missing markets field. We skip these markets manually when retrieving.
72
74
  tags: list[PolymarketGammaTag]
73
75
 
74
76
  @property
@@ -7,6 +7,7 @@ from prediction_market_agent_tooling.gtypes import (
7
7
  OutcomeStr,
8
8
  Probability,
9
9
  )
10
+ from prediction_market_agent_tooling.loggers import logger
10
11
  from prediction_market_agent_tooling.markets.agent_market import (
11
12
  AgentMarket,
12
13
  ConditionalFilterType,
@@ -31,6 +32,7 @@ from prediction_market_agent_tooling.markets.polymarket.polymarket_subgraph_hand
31
32
  PolymarketSubgraphHandler,
32
33
  )
33
34
  from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
35
+ from prediction_market_agent_tooling.tools.utils import check_not_none
34
36
 
35
37
 
36
38
  class PolymarketAgentMarket(AgentMarket):
@@ -69,9 +71,11 @@ class PolymarketAgentMarket(AgentMarket):
69
71
  ]
70
72
  # For a binary market, there should be exactly one payout numerator greater than 0.
71
73
  if len(payout_numerator_indices_gt_0) != 1:
72
- raise ValueError(
73
- f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators}"
74
+ # These cases involve multi-categorical resolution (to be implemented https://github.com/gnosis/prediction-market-agent-tooling/issues/770)
75
+ logger.warning(
76
+ f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators} for condition_id {condition_id.hex()}"
74
77
  )
78
+ return Resolution(outcome=None, invalid=False)
75
79
 
76
80
  # we return the only payout numerator greater than 0 as resolution
77
81
  resolved_outcome = outcomes[payout_numerator_indices_gt_0[0]]
@@ -83,16 +87,16 @@ class PolymarketAgentMarket(AgentMarket):
83
87
  condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
84
88
  ) -> "PolymarketAgentMarket":
85
89
  # If len(model.markets) > 0, this denotes a categorical market.
86
-
87
- outcomes = model.markets[0].outcomes_list
88
- outcome_prices = model.markets[0].outcome_prices
90
+ markets = check_not_none(model.markets)
91
+ outcomes = markets[0].outcomes_list
92
+ outcome_prices = markets[0].outcome_prices
89
93
  if not outcome_prices:
90
94
  # We give random prices
91
95
  outcome_prices = [0.5, 0.5]
92
96
  probabilities = {o: Probability(op) for o, op in zip(outcomes, outcome_prices)}
93
97
 
94
98
  resolution = PolymarketAgentMarket.build_resolution_from_condition(
95
- condition_id=model.markets[0].conditionId,
99
+ condition_id=markets[0].conditionId,
96
100
  condition_model_dict=condition_model_dict,
97
101
  outcomes=outcomes,
98
102
  )
@@ -164,7 +168,11 @@ class PolymarketAgentMarket(AgentMarket):
164
168
  )
165
169
 
166
170
  condition_models = PolymarketSubgraphHandler().get_conditions(
167
- condition_ids=[market.markets[0].conditionId for market in markets]
171
+ condition_ids=[
172
+ market.markets[0].conditionId
173
+ for market in markets
174
+ if market.markets is not None
175
+ ]
168
176
  )
169
177
  condition_models_dict = {c.id: c for c in condition_models}
170
178
 
@@ -2,6 +2,7 @@ import asyncio
2
2
  import typing as t
3
3
  from datetime import timedelta
4
4
 
5
+ import cachetools
5
6
  from cowdao_cowpy.common.api.errors import UnexpectedResponseError
6
7
  from eth_typing import ChecksumAddress
7
8
  from web3 import Web3
@@ -99,6 +100,11 @@ from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow
99
100
  SEER_TINY_BET_AMOUNT = USD(0.1)
100
101
 
101
102
 
103
+ SHARED_CACHE: cachetools.TTLCache[t.Hashable, t.Any] = cachetools.TTLCache(
104
+ maxsize=256, ttl=10 * 60
105
+ )
106
+
107
+
102
108
  class SeerAgentMarket(AgentMarket):
103
109
  wrapped_tokens: list[ChecksumAddress]
104
110
  creator: HexAddress
@@ -541,6 +547,7 @@ class SeerAgentMarket(AgentMarket):
541
547
  liquidity = self.get_liquidity_for_outcome(outcome)
542
548
  return liquidity > self.minimum_market_liquidity_required
543
549
 
550
+ @cachetools.cached(cache=SHARED_CACHE, key=lambda self: f"has_liquidity_{self.id}")
544
551
  def has_liquidity(self) -> bool:
545
552
  # We define a market as having liquidity if it has liquidity for all outcomes except for the invalid (index -1)
546
553
  return all(
@@ -594,7 +601,7 @@ class SeerAgentMarket(AgentMarket):
594
601
  f"Expected exactly 1 trade from {order_metadata=}, but got {len(trades)=}."
595
602
  )
596
603
  cow_tx_hash = trades[0].txHash
597
- logger.info(f"TxHash for {order_metadata.uid.root=} is {cow_tx_hash=}.")
604
+ logger.info(f"TxHash is {cow_tx_hash=} for {order_metadata.uid.root=}.")
598
605
  return cow_tx_hash.hex()
599
606
 
600
607
  except (
@@ -626,7 +633,9 @@ class SeerAgentMarket(AgentMarket):
626
633
  amount_wei=amount_wei,
627
634
  web3=web3,
628
635
  )
629
- return tx_receipt["transactionHash"].hex()
636
+ swap_pool_tx_hash = tx_receipt["transactionHash"].hex()
637
+ logger.info(f"TxHash is {swap_pool_tx_hash=}.")
638
+ return swap_pool_tx_hash
630
639
 
631
640
  def place_bet(
632
641
  self,
@@ -71,7 +71,10 @@ class SwapPoolHandler:
71
71
  price_outcome_token = PriceManager.build(
72
72
  HexBytes(HexStr(self.market_id))
73
73
  ).get_token_price_from_pools(token=outcome_token)
74
- if not price_outcome_token:
74
+ if (
75
+ not price_outcome_token
76
+ or not price_outcome_token.priceOfCollateralInAskingToken
77
+ ):
75
78
  raise ValueError(
76
79
  f"Could not find price for {outcome_token=} and {self.collateral_token_address}"
77
80
  )
@@ -1,6 +1,24 @@
1
- from prediction_market_agent_tooling.gtypes import CollateralToken, OutcomeToken
1
+ from itertools import chain
2
+
3
+ import numpy as np
4
+ from scipy.optimize import minimize
5
+
6
+ from prediction_market_agent_tooling.gtypes import (
7
+ CollateralToken,
8
+ OutcomeToken,
9
+ Probability,
10
+ )
11
+ from prediction_market_agent_tooling.loggers import logger
12
+ from prediction_market_agent_tooling.markets.agent_market import AgentMarket
2
13
  from prediction_market_agent_tooling.markets.market_fees import MarketFees
3
- from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet
14
+ from prediction_market_agent_tooling.markets.omen.omen import (
15
+ calculate_buy_outcome_token,
16
+ )
17
+ from prediction_market_agent_tooling.tools.betting_strategies.utils import (
18
+ BinaryKellyBet,
19
+ CategoricalKellyBet,
20
+ )
21
+ from prediction_market_agent_tooling.tools.utils import check_not_none
4
22
 
5
23
 
6
24
  def check_is_valid_probability(probability: float) -> None:
@@ -13,7 +31,7 @@ def get_kelly_bet_simplified(
13
31
  market_p_yes: float,
14
32
  estimated_p_yes: float,
15
33
  confidence: float,
16
- ) -> SimpleBet:
34
+ ) -> BinaryKellyBet:
17
35
  """
18
36
  Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
19
37
 
@@ -39,22 +57,24 @@ def get_kelly_bet_simplified(
39
57
  if estimated_p_yes > market_p_yes:
40
58
  bet_direction = True
41
59
  market_prob = market_p_yes
60
+ estimated_p = estimated_p_yes
42
61
  else:
43
62
  bet_direction = False
44
63
  market_prob = 1 - market_p_yes
64
+ estimated_p = 1 - estimated_p_yes
45
65
 
46
66
  # Handle the case where market_prob is 0
47
67
  if market_prob == 0:
48
68
  market_prob = 1e-10
49
69
 
50
- edge = abs(estimated_p_yes - market_p_yes) * confidence
70
+ edge = abs(estimated_p - market_prob) * confidence
51
71
  odds = (1 / market_prob) - 1
52
72
  kelly_fraction = edge / odds
53
73
 
54
74
  # Ensure bet size is non-negative does not exceed the wallet balance
55
75
  bet_size = CollateralToken(min(kelly_fraction * max_bet.value, max_bet.value))
56
76
 
57
- return SimpleBet(direction=bet_direction, size=bet_size)
77
+ return BinaryKellyBet(direction=bet_direction, size=bet_size)
58
78
 
59
79
 
60
80
  def get_kelly_bet_full(
@@ -64,7 +84,7 @@ def get_kelly_bet_full(
64
84
  confidence: float,
65
85
  max_bet: CollateralToken,
66
86
  fees: MarketFees,
67
- ) -> SimpleBet:
87
+ ) -> BinaryKellyBet:
68
88
  """
69
89
  Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
70
90
 
@@ -98,7 +118,7 @@ def get_kelly_bet_full(
98
118
  check_is_valid_probability(confidence)
99
119
 
100
120
  if max_bet == 0:
101
- return SimpleBet(size=CollateralToken(0), direction=True)
121
+ return BinaryKellyBet(size=CollateralToken(0), direction=True)
102
122
 
103
123
  x = yes_outcome_pool_size.value
104
124
  y = no_outcome_pool_size.value
@@ -144,7 +164,255 @@ def get_kelly_bet_full(
144
164
  kelly_bet_amount = numerator / denominator
145
165
 
146
166
  # Clip the bet size to max_bet to account for rounding errors.
147
- return SimpleBet(
167
+ return BinaryKellyBet(
148
168
  direction=kelly_bet_amount > 0,
149
169
  size=CollateralToken(min(max_bet.value, abs(kelly_bet_amount))),
150
170
  )
171
+
172
+
173
+ def get_kelly_bets_categorical_simplified(
174
+ market_probabilities: list[Probability],
175
+ estimated_probabilities: list[Probability],
176
+ confidence: float,
177
+ max_bet: CollateralToken,
178
+ fees: MarketFees,
179
+ allow_multiple_bets: bool,
180
+ allow_shorting: bool,
181
+ bet_precision: int = 6,
182
+ ) -> list[CategoricalKellyBet]:
183
+ """
184
+ Calculate Kelly bets for categorical markets using only market probabilities.
185
+ Returns a list of CategoricalKellyBet objects, one for each outcome.
186
+ Considers max_bet across all outcomes together.
187
+ Indicates both buying (long) and shorting (selling) by allowing negative bet sizes.
188
+ """
189
+ for p in chain(market_probabilities, estimated_probabilities, [confidence]):
190
+ check_is_valid_probability(p)
191
+ assert len(market_probabilities) == len(
192
+ estimated_probabilities
193
+ ), "Mismatch in number of outcomes"
194
+
195
+ f = 1 - fees.bet_proportion
196
+
197
+ total_kelly_fraction = 0.0
198
+ kelly_fractions = []
199
+
200
+ for i in range(len(market_probabilities)):
201
+ estimated_p = estimated_probabilities[i]
202
+ market_p = max(market_probabilities[i], 1e-10)
203
+
204
+ edge = (estimated_p - market_p) * confidence
205
+ odds = (1 / market_p) - 1
206
+ kelly_fraction = edge / odds * f
207
+
208
+ if not allow_shorting:
209
+ kelly_fraction = max(0, kelly_fraction)
210
+
211
+ kelly_fractions.append(kelly_fraction)
212
+ total_kelly_fraction += abs(kelly_fraction)
213
+
214
+ best_kelly_fraction_index = max(
215
+ range(len(kelly_fractions)), key=lambda i: abs(kelly_fractions[i])
216
+ )
217
+
218
+ bets = []
219
+ for i, kelly_fraction in enumerate(kelly_fractions):
220
+ if not allow_multiple_bets:
221
+ bet_size = (
222
+ kelly_fraction * max_bet.value if i == best_kelly_fraction_index else 0
223
+ )
224
+ elif allow_multiple_bets and total_kelly_fraction > 0:
225
+ bet_size = (kelly_fraction / total_kelly_fraction) * max_bet.value
226
+ else:
227
+ bet_size = 0.0
228
+ # Ensure bet_size is within [-max_bet.value, max_bet.value]
229
+ bet_size = max(-max_bet.value, min(bet_size, max_bet.value))
230
+ bets.append(
231
+ CategoricalKellyBet(
232
+ index=i, size=CollateralToken(round(bet_size, bet_precision))
233
+ )
234
+ )
235
+
236
+ return bets
237
+
238
+
239
+ def get_kelly_bets_categorical_full(
240
+ outcome_pool_sizes: list[OutcomeToken],
241
+ estimated_probabilities: list[Probability],
242
+ confidence: float,
243
+ max_bet: CollateralToken,
244
+ fees: MarketFees,
245
+ allow_multiple_bets: bool,
246
+ allow_shorting: bool,
247
+ multicategorical: bool,
248
+ bet_precision: int = 6,
249
+ ) -> list[CategoricalKellyBet]:
250
+ """
251
+ Calculate Kelly bets for categorical markets using joint optimization over all outcomes,
252
+ splitting the max bet between all possible outcomes to maximize expected log utility.
253
+ Returns a list of CategoricalKellyBet objects, one for each outcome.
254
+ Handles both buying (long) and shorting (selling) by allowing negative bet sizes.
255
+ If the agent's probabilities are very close to the market's, returns all-zero bets.
256
+ multicategorical means that multiple outcomes could be selected as correct ones.
257
+ """
258
+ assert len(outcome_pool_sizes) == len(
259
+ estimated_probabilities
260
+ ), "Mismatch in number of outcomes"
261
+
262
+ market_probabilities = AgentMarket.compute_fpmm_probabilities(
263
+ [x.as_outcome_wei for x in outcome_pool_sizes]
264
+ )
265
+
266
+ for p in chain(market_probabilities, estimated_probabilities, [confidence]):
267
+ check_is_valid_probability(p)
268
+
269
+ n = len(outcome_pool_sizes)
270
+ max_bet_value = max_bet.value
271
+
272
+ if all(
273
+ abs(estimated_probabilities[i] - market_probabilities[i]) < 1e-3
274
+ for i in range(n)
275
+ ):
276
+ return [
277
+ CategoricalKellyBet(index=i, size=CollateralToken(0.0)) for i in range(n)
278
+ ]
279
+
280
+ def compute_payouts(bets: list[float]) -> list[float]:
281
+ payouts: list[float] = []
282
+ for i in range(n):
283
+ payout = 0.0
284
+ if bets[i] >= 0:
285
+ # If bet on i is positive, we buy outcome i
286
+ buy_result = calculate_buy_outcome_token(
287
+ CollateralToken(bets[i]), i, outcome_pool_sizes, fees
288
+ )
289
+ payout += buy_result.outcome_tokens_received.value
290
+ else:
291
+ # If bet is negative, we "short" outcome i by buying all other outcomes
292
+ for j in range(n):
293
+ if j == i:
294
+ continue
295
+ buy_result = calculate_buy_outcome_token(
296
+ CollateralToken(abs(bets[i]) / (n - 1)),
297
+ j,
298
+ outcome_pool_sizes,
299
+ fees,
300
+ )
301
+ payout += buy_result.outcome_tokens_received.value
302
+ payouts.append(payout)
303
+ return payouts
304
+
305
+ def adjust_prob(my_prob: float, market_prob: float) -> float:
306
+ # Based on the confidence, shrinks the predicted probability towards market's current probability.
307
+ return confidence * my_prob + (1 - confidence) * market_prob
308
+
309
+ # Use the simple version to estimate the initial bet vector.
310
+ x0 = np.array(
311
+ [
312
+ x.size.value # Use simplified value as starting point
313
+ for x in get_kelly_bets_categorical_simplified(
314
+ market_probabilities=market_probabilities,
315
+ estimated_probabilities=estimated_probabilities,
316
+ confidence=confidence,
317
+ max_bet=max_bet,
318
+ fees=fees,
319
+ allow_multiple_bets=allow_multiple_bets,
320
+ allow_shorting=allow_shorting,
321
+ bet_precision=bet_precision,
322
+ )
323
+ ]
324
+ )
325
+
326
+ # Track the best solution found during optimization
327
+ best_solution_bets = None
328
+ best_solution_utility = float("-inf")
329
+
330
+ def neg_expected_log_utility(bets: list[float]) -> float:
331
+ """
332
+ Negative expected log utility for categorical Kelly betting.
333
+ This function is minimized to find the optimal bet allocation.
334
+ """
335
+ adj_probs = [
336
+ adjust_prob(estimated_probabilities[i], market_probabilities[i])
337
+ for i in range(n)
338
+ ]
339
+ payouts = compute_payouts(bets)
340
+
341
+ profits = [payout - abs(bet) for payout, bet in zip(payouts, bets)]
342
+
343
+ # Ensure profits are not too negative to avoid log(negative) or log(0)
344
+ # Use a small epsilon to prevent numerical instability
345
+ min_profit = -0.99 # Ensure 1 + profit > 0.01
346
+ profits = [max(profit, min_profit) for profit in profits]
347
+
348
+ # Expected log utility
349
+ expected_log_utility: float = sum(
350
+ adj_probs[i] * np.log(1 + profits[i]) for i in range(n)
351
+ )
352
+
353
+ # Track the best solution found so far
354
+ nonlocal best_solution_bets, best_solution_utility
355
+ if expected_log_utility > best_solution_utility:
356
+ best_solution_bets = np.array(bets)
357
+ best_solution_utility = expected_log_utility
358
+
359
+ # Return negative for minimization
360
+ return -expected_log_utility
361
+
362
+ constraints = [
363
+ # We can not bet more than `max_bet_value`
364
+ {
365
+ "type": "ineq",
366
+ "fun": lambda bets: max_bet_value - np.sum(np.abs(bets)),
367
+ },
368
+ # Each bet should not result in guaranteed loss
369
+ {
370
+ "type": "ineq",
371
+ "fun": lambda bets: [
372
+ payout
373
+ - (sum(abs(b) for b in bets) if not multicategorical else abs(bets[i]))
374
+ for i, payout in enumerate(compute_payouts(bets))
375
+ ],
376
+ },
377
+ ]
378
+
379
+ result = minimize(
380
+ neg_expected_log_utility,
381
+ x0,
382
+ method="SLSQP",
383
+ bounds=[
384
+ ((-max_bet_value if allow_shorting else 0), max_bet_value) for _ in range(n)
385
+ ],
386
+ constraints=constraints,
387
+ options={"maxiter": 10_000},
388
+ )
389
+
390
+ # This can sometimes happen, as long as it's occasional, it's should be fine to just use simplified version approximation.
391
+ if not result.success:
392
+ logger.warning(
393
+ f"Joint optimization failed: {result=} {x0=} {estimated_probabilities=} {confidence=} {market_probabilities=}"
394
+ )
395
+
396
+ # Use the best solution found during optimization, not just the final result (result.x).
397
+ # This is important because SLSQP may end on a worse solution due to numerical issues.
398
+ bet_vector = check_not_none(best_solution_bets) if result.success else x0
399
+
400
+ if not allow_multiple_bets:
401
+ # If we are not allowing multiple bets, we need to ensure only one bet is non-zero.
402
+ # We can do this by taking the maximum bet and setting all others to zero.
403
+ # We do this, instead of enforcing it in with additional constraint,
404
+ # because such hard constraint is problematic for the solver and results in almost always failing to optimize.
405
+ max_bet_index = np.argmax(np.abs(bet_vector))
406
+ max_bet_value = bet_vector[max_bet_index]
407
+
408
+ bet_vector = np.zeros_like(bet_vector)
409
+ bet_vector[max_bet_index] = max_bet_value
410
+
411
+ bets = [
412
+ CategoricalKellyBet(
413
+ index=i, size=CollateralToken(round(bet_vector[i], bet_precision))
414
+ )
415
+ for i in range(n)
416
+ ]
417
+
418
+ return bets
@@ -3,6 +3,11 @@ from pydantic import BaseModel
3
3
  from prediction_market_agent_tooling.gtypes import CollateralToken
4
4
 
5
5
 
6
- class SimpleBet(BaseModel):
6
+ class BinaryKellyBet(BaseModel):
7
7
  direction: bool
8
8
  size: CollateralToken
9
+
10
+
11
+ class CategoricalKellyBet(BaseModel):
12
+ index: int
13
+ size: CollateralToken
@@ -60,6 +60,15 @@ class HexBytes(HexBytesBase, BaseHex):
60
60
  value = hex_str[2:] if hex_str.startswith("0x") else hex_str
61
61
  return super().fromhex(value)
62
62
 
63
+ def hex(
64
+ self,
65
+ sep: t.Union[str, bytes] | None = None,
66
+ bytes_per_sep: t.SupportsIndex = 1,
67
+ ) -> str:
68
+ """We enforce a 0x prefix."""
69
+ x = super().hex(sep, bytes_per_sep) # type: ignore[arg-type]
70
+ return x if x.startswith("0x") else "0x" + x
71
+
63
72
  @classmethod
64
73
  def __eth_pydantic_validate__(
65
74
  cls, value: t.Any, info: ValidationInfo | None = None
@@ -1,15 +1,17 @@
1
+ from datetime import timedelta
2
+
1
3
  import hishel
2
4
  import httpx
3
5
 
4
6
  from prediction_market_agent_tooling.tools.singleton import SingletonMeta
5
7
 
6
- ONE_DAY_IN_SECONDS = 60 * 60 * 24
8
+ ONE_DAY = timedelta(days=1)
7
9
 
8
10
 
9
11
  class HttpxCachedClient(metaclass=SingletonMeta):
10
- def __init__(self, ttl: int = ONE_DAY_IN_SECONDS) -> None:
12
+ def __init__(self, ttl: timedelta = ONE_DAY) -> None:
11
13
  storage = hishel.FileStorage(
12
- ttl=ttl,
14
+ ttl=ttl.total_seconds(),
13
15
  check_ttl_every=60,
14
16
  )
15
17
  controller = hishel.Controller(force_cache=True)
@@ -68,6 +68,7 @@ def get_traces_for_agent(
68
68
  client: Langfuse,
69
69
  to_timestamp: DatetimeUTC | None = None,
70
70
  tags: str | list[str] | None = None,
71
+ limit: int | None = None,
71
72
  ) -> list[TraceWithDetails]:
72
73
  """
73
74
  Fetch agent traces using pagination
@@ -98,6 +99,9 @@ def get_traces_for_agent(
98
99
  if has_output:
99
100
  agent_traces = [t for t in agent_traces if t.output is not None]
100
101
  all_agent_traces.extend(agent_traces)
102
+ if limit is not None and len(all_agent_traces) >= limit:
103
+ all_agent_traces = all_agent_traces[:limit]
104
+ break
101
105
  return all_agent_traces
102
106
 
103
107
 
@@ -155,9 +159,6 @@ def get_trace_for_bet(
155
159
  not in WRAPPED_XDAI_CONTRACT_ADDRESS
156
160
  ):
157
161
  # TODO: We need to compute bet amount token in USD here, but at the time of bet placement!
158
- logger.warning(
159
- "This currently works only for WXDAI markets, because we need to compare against USD value."
160
- )
161
162
  continue
162
163
  # Cannot use exact comparison due to gas fees
163
164
  if (
@@ -8,16 +8,21 @@ class SingletonMeta(type, t.Generic[_T]):
8
8
  The Singleton class can be implemented in different ways in Python. Some
9
9
  possible methods include: base class, decorator, metaclass. We will use the
10
10
  metaclass because it is best suited for this purpose.
11
+
12
+ This version creates a unique instance for each unique set of __init__ arguments.
11
13
  """
12
14
 
13
- _instances: dict[t.Any, _T] = {}
15
+ _instances: dict[
16
+ tuple[t.Any, tuple[t.Any, ...], tuple[tuple[str, t.Any], ...]], _T
17
+ ] = {}
14
18
 
15
19
  def __call__(self, *args: t.Any, **kwargs: t.Any) -> _T:
16
20
  """
17
- Possible changes to the value of the `__init__` argument do not affect
18
- the returned instance.
21
+ Different __init__ arguments will result in different instances.
19
22
  """
20
- if self not in self._instances:
23
+ # Create a key based on the class, args, and kwargs (sorted for consistency)
24
+ key = (self, args, tuple(sorted(kwargs.items())))
25
+ if key not in self._instances:
21
26
  instance = super().__call__(*args, **kwargs)
22
- self._instances[self] = instance
23
- return self._instances[self]
27
+ self._instances[key] = instance
28
+ return self._instances[key]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prediction-market-agent-tooling
3
- Version: 0.67.3
3
+ Version: 0.67.4.dev992
4
4
  Summary: Tools to benchmark, deploy and monitor prediction market agents.
5
5
  Author: Gnosis
6
6
  Requires-Python: >=3.10,<3.13