prediction-market-agent-tooling 0.66.0.dev791__py3-none-any.whl → 0.66.2__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.
@@ -571,7 +571,9 @@ class DeployableTraderAgent(DeployablePredictionAgent):
571
571
  Given the market and prediction, agent uses this method to calculate optimal outcome and bet size.
572
572
  """
573
573
  total_amount = self.get_total_amount_to_bet(market)
574
- return MultiCategoricalMaxAccuracyBettingStrategy(bet_amount=total_amount)
574
+ return MultiCategoricalMaxAccuracyBettingStrategy(
575
+ max_position_amount=total_amount
576
+ )
575
577
 
576
578
  def build_trades(
577
579
  self,
@@ -1,6 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Sequence
3
3
 
4
+ import numpy as np
4
5
  from scipy.optimize import minimize_scalar
5
6
 
6
7
  from prediction_market_agent_tooling.benchmark.utils import get_most_probable_outcome
@@ -94,6 +95,76 @@ class BettingStrategy(ABC):
94
95
  )
95
96
  return trades
96
97
 
98
+ @staticmethod
99
+ def cap_to_profitable_bet_amount(
100
+ market: AgentMarket,
101
+ bet_amount: USD,
102
+ outcome: OutcomeStr,
103
+ iters: int = 10,
104
+ ) -> USD:
105
+ """
106
+ Use a binary search (tree-based search) to efficiently find the largest profitable bet amount.
107
+ """
108
+ # First, try it with the desired amount right away.
109
+ if (
110
+ market.get_in_usd(
111
+ check_not_none(
112
+ market.get_buy_token_amount(bet_amount, outcome)
113
+ ).as_token
114
+ )
115
+ > bet_amount
116
+ ):
117
+ return bet_amount
118
+
119
+ # If it wasn't profitable, try binary search to find the highest, but profitable, amount.
120
+ lower = USD(0)
121
+ # It doesn't make sense to try to bet more than the liquidity itself, so override it as maximal value if it's lower.
122
+ upper = min(bet_amount, market.get_in_usd(market.get_liquidity()))
123
+ best_profitable = USD(0)
124
+
125
+ for _ in range(iters):
126
+ mid = (lower + upper) / 2
127
+ potential_outcome_value = market.get_in_usd(
128
+ check_not_none(market.get_buy_token_amount(mid, outcome)).as_token
129
+ )
130
+
131
+ if potential_outcome_value > mid:
132
+ # Profitable, try higher
133
+ best_profitable = mid
134
+ lower = mid
135
+
136
+ else:
137
+ # Not profitable, try lower
138
+ upper = mid
139
+
140
+ # If the search interval is very small, break early
141
+ if float(upper - lower) < 1e-8:
142
+ break
143
+
144
+ if np.isclose(best_profitable.value, 0):
145
+ best_profitable = USD(0)
146
+
147
+ return best_profitable
148
+
149
+ @staticmethod
150
+ def cap_to_profitable_position(
151
+ market: AgentMarket,
152
+ existing_position: USD,
153
+ wanted_position: USD,
154
+ outcome_to_bet_on: OutcomeStr,
155
+ ) -> USD:
156
+ # If the wanted position is lower, it means the agent is gonna sell and that's profitable always.
157
+ if wanted_position > existing_position:
158
+ difference = wanted_position - existing_position
159
+ # Cap the difference we would like to buy to a profitable one.
160
+ capped_difference = BettingStrategy.cap_to_profitable_bet_amount(
161
+ market, difference, outcome_to_bet_on
162
+ )
163
+ # Lowered the actual wanted position such that it remains profitable.
164
+ wanted_position = existing_position + capped_difference
165
+
166
+ return wanted_position
167
+
97
168
  def _build_rebalance_trades_from_positions(
98
169
  self,
99
170
  existing_position: ExistingPosition | None,
@@ -160,12 +231,12 @@ class BettingStrategy(ABC):
160
231
 
161
232
 
162
233
  class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
163
- def __init__(self, bet_amount: USD):
164
- self.bet_amount = bet_amount
234
+ def __init__(self, max_position_amount: USD):
235
+ self.max_position_amount = max_position_amount
165
236
 
166
237
  @property
167
238
  def maximum_possible_bet_amount(self) -> USD:
168
- return self.bet_amount
239
+ return self.max_position_amount
169
240
 
170
241
  @staticmethod
171
242
  def calculate_direction(
@@ -196,11 +267,23 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
196
267
  market: AgentMarket,
197
268
  ) -> list[Trade]:
198
269
  """We place bet on only one outcome."""
199
-
200
270
  outcome_to_bet_on = self.calculate_direction(market, answer)
201
271
 
272
+ # Will be lowered if the amount that we would need to buy would be unprofitable.
273
+ actual_wanted_position = BettingStrategy.cap_to_profitable_position(
274
+ market,
275
+ (
276
+ existing_position.amounts_current.get(outcome_to_bet_on, USD(0))
277
+ if existing_position
278
+ else USD(0)
279
+ ),
280
+ self.max_position_amount,
281
+ outcome_to_bet_on,
282
+ )
283
+
202
284
  target_position = Position(
203
- market_id=market.id, amounts_current={outcome_to_bet_on: self.bet_amount}
285
+ market_id=market.id,
286
+ amounts_current={outcome_to_bet_on: actual_wanted_position},
204
287
  )
205
288
  trades = self._build_rebalance_trades_from_positions(
206
289
  existing_position=existing_position,
@@ -253,13 +336,13 @@ class MaxExpectedValueBettingStrategy(MultiCategoricalMaxAccuracyBettingStrategy
253
336
 
254
337
 
255
338
  class KellyBettingStrategy(BettingStrategy):
256
- def __init__(self, max_bet_amount: USD, max_price_impact: float | None = None):
257
- self.max_bet_amount = max_bet_amount
339
+ def __init__(self, max_position_amount: USD, max_price_impact: float | None = None):
340
+ self.max_position_amount = max_position_amount
258
341
  self.max_price_impact = max_price_impact
259
342
 
260
343
  @property
261
344
  def maximum_possible_bet_amount(self) -> USD:
262
- return self.max_bet_amount
345
+ return self.max_position_amount
263
346
 
264
347
  @staticmethod
265
348
  def get_kelly_bet(
@@ -321,7 +404,7 @@ class KellyBettingStrategy(BettingStrategy):
321
404
 
322
405
  kelly_bet = self.get_kelly_bet(
323
406
  market=market,
324
- max_bet_amount=self.max_bet_amount,
407
+ max_bet_amount=self.max_position_amount,
325
408
  direction=direction,
326
409
  other_direction=other_direction,
327
410
  answer=answer,
@@ -405,16 +488,16 @@ class KellyBettingStrategy(BettingStrategy):
405
488
  return CollateralToken(optimized_bet_amount.x)
406
489
 
407
490
  def __repr__(self) -> str:
408
- return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount}, max_price_impact={self.max_price_impact})"
491
+ return f"{self.__class__.__name__}(max_bet_amount={self.max_position_amount}, max_price_impact={self.max_price_impact})"
409
492
 
410
493
 
411
494
  class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
412
- def __init__(self, max_bet_amount: USD):
413
- self.max_bet_amount = max_bet_amount
495
+ def __init__(self, max_position_amount: USD):
496
+ self.max_position_amount = max_position_amount
414
497
 
415
498
  @property
416
499
  def maximum_possible_bet_amount(self) -> USD:
417
- return self.max_bet_amount
500
+ return self.max_position_amount
418
501
 
419
502
  def adjust_bet_amount(
420
503
  self, existing_position: ExistingPosition | None, market: AgentMarket
@@ -422,7 +505,7 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
422
505
  existing_position_total_amount = (
423
506
  existing_position.total_amount_current if existing_position else USD(0)
424
507
  )
425
- return self.max_bet_amount + existing_position_total_amount
508
+ return self.max_position_amount + existing_position_total_amount
426
509
 
427
510
  def calculate_trades(
428
511
  self,
@@ -471,4 +554,4 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
471
554
  return trades
472
555
 
473
556
  def __repr__(self) -> str:
474
- return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount})"
557
+ return f"{self.__class__.__name__}(max_bet_amount={self.max_position_amount})"
@@ -93,7 +93,7 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
93
93
 
94
94
  def get_job_trade(self, max_bond: USD, result: str) -> Trade:
95
95
  # Because jobs are powered by prediction markets, potentional reward depends on job's liquidity and our will to bond (bet) our xDai into our job completion.
96
- strategy = KellyBettingStrategy(max_bet_amount=max_bond)
96
+ strategy = KellyBettingStrategy(max_position_amount=max_bond)
97
97
  required_trades = strategy.calculate_trades(
98
98
  existing_position=None,
99
99
  answer=self.get_job_answer(result),
@@ -15,7 +15,7 @@ from prediction_market_agent_tooling.gtypes import (
15
15
  Probability,
16
16
  )
17
17
  from prediction_market_agent_tooling.logprobs_parser import FieldLogprobs
18
- from prediction_market_agent_tooling.markets.omen.omen import (
18
+ from prediction_market_agent_tooling.markets.omen.omen_constants import (
19
19
  OMEN_FALSE_OUTCOME,
20
20
  OMEN_TRUE_OUTCOME,
21
21
  )
@@ -14,13 +14,13 @@ class QuestionType(str, Enum):
14
14
  class AggregationItem(BaseModel):
15
15
  start_time: DatetimeUTC
16
16
  end_time: DatetimeUTC | None
17
- forecast_values: list[float] | None
17
+ forecast_values: list[float] | None = None
18
18
  forecaster_count: int
19
19
  interval_lower_bounds: list[float] | None
20
20
  centers: list[float] | None
21
21
  interval_upper_bounds: list[float] | None
22
- means: list[float] | None
23
- histogram: list[list[float]] | None
22
+ means: list[float] | None = None
23
+ histogram: list[list[float]] | None = None
24
24
 
25
25
 
26
26
  class Aggregation(BaseModel):
@@ -3,7 +3,7 @@ import os
3
3
  from web3 import Web3
4
4
 
5
5
  from prediction_market_agent_tooling.config import APIKeys
6
- from prediction_market_agent_tooling.gtypes import ABI, HexBytes
6
+ from prediction_market_agent_tooling.gtypes import ABI, ChecksumAddress, HexBytes
7
7
  from prediction_market_agent_tooling.tools.contract import (
8
8
  ContractOnGnosisChain,
9
9
  abi_field_validator,
@@ -19,6 +19,10 @@ class CowGPv2SettlementContract(ContractOnGnosisChain):
19
19
  )
20
20
  )
21
21
 
22
+ address: ChecksumAddress = Web3.to_checksum_address(
23
+ "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
24
+ )
25
+
22
26
  def setPreSignature(
23
27
  self,
24
28
  api_keys: APIKeys,
@@ -23,6 +23,10 @@ from prediction_market_agent_tooling.markets.data_models import (
23
23
  Resolution,
24
24
  ResolvedBet,
25
25
  )
26
+ from prediction_market_agent_tooling.markets.omen.omen_constants import (
27
+ OMEN_FALSE_OUTCOME,
28
+ OMEN_TRUE_OUTCOME,
29
+ )
26
30
  from prediction_market_agent_tooling.tools.contract import (
27
31
  ContractERC20OnGnosisChain,
28
32
  init_collateral_token_contract,
@@ -37,8 +41,6 @@ from prediction_market_agent_tooling.tools.utils import (
37
41
  utcnow,
38
42
  )
39
43
 
40
- OMEN_TRUE_OUTCOME = OutcomeStr("Yes")
41
- OMEN_FALSE_OUTCOME = OutcomeStr("No")
42
44
  OMEN_BINARY_MARKET_OUTCOMES: t.Sequence[OutcomeStr] = [
43
45
  OMEN_TRUE_OUTCOME,
44
46
  OMEN_FALSE_OUTCOME,
@@ -104,6 +104,7 @@ class OmenAgentMarket(AgentMarket):
104
104
 
105
105
  collateral_token_contract_address_checksummed: ChecksumAddress
106
106
  market_maker_contract_address_checksummed: ChecksumAddress
107
+ outcome_token_pool: dict[OutcomeStr, OutcomeToken]
107
108
  condition: Condition
108
109
  finalized_time: DatetimeUTC | None
109
110
  created_time: DatetimeUTC
@@ -620,11 +621,10 @@ class OmenAgentMarket(AgentMarket):
620
621
  Note: this is only valid if the market instance's token pool is
621
622
  up-to-date with the smart contract.
622
623
  """
623
- outcome_token_pool = check_not_none(self.outcome_token_pool)
624
624
  amount = get_buy_outcome_token_amount(
625
625
  investment_amount=self.get_in_token(bet_amount),
626
626
  outcome_index=self.get_outcome_index(outcome),
627
- pool_balances=[outcome_token_pool[x] for x in self.outcomes],
627
+ pool_balances=[self.outcome_token_pool[x] for x in self.outcomes],
628
628
  fees=self.fees,
629
629
  )
630
630
  return amount
@@ -1,5 +1,10 @@
1
1
  from web3 import Web3
2
2
 
3
+ from prediction_market_agent_tooling.gtypes import OutcomeStr
4
+
5
+ OMEN_TRUE_OUTCOME = OutcomeStr("Yes")
6
+ OMEN_FALSE_OUTCOME = OutcomeStr("No")
7
+
3
8
  WRAPPED_XDAI_CONTRACT_ADDRESS = Web3.to_checksum_address(
4
9
  "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d"
5
10
  )
@@ -2,6 +2,7 @@ import asyncio
2
2
  import typing as t
3
3
  from datetime import timedelta
4
4
 
5
+ from cowdao_cowpy.common.api.errors import UnexpectedResponseError
5
6
  from eth_typing import ChecksumAddress
6
7
  from web3 import Web3
7
8
  from web3.types import TxReceipt
@@ -61,7 +62,6 @@ from prediction_market_agent_tooling.tools.contract import (
61
62
  to_gnosis_chain_contract,
62
63
  )
63
64
  from prediction_market_agent_tooling.tools.cow.cow_order import (
64
- cancel_order,
65
65
  get_buy_token_amount_else_raise,
66
66
  get_orders_by_owner,
67
67
  get_trades_by_owner,
@@ -198,9 +198,7 @@ class SeerAgentMarket(AgentMarket):
198
198
  )
199
199
  )
200
200
 
201
- amounts_ot[
202
- OutcomeStr(outcome_str)
203
- ] = outcome_token_balance_wei.as_outcome_token
201
+ amounts_ot[outcome_str] = outcome_token_balance_wei.as_outcome_token
204
202
 
205
203
  amounts_current = {
206
204
  k: self.get_token_in_usd(self.get_sell_value_of_outcome_token(k, v))
@@ -481,26 +479,32 @@ class SeerAgentMarket(AgentMarket):
481
479
  Returns:
482
480
  Transaction hash of the successful swap
483
481
  """
484
- _, order = swap_tokens_waiting(
485
- amount_wei=amount_wei,
486
- sell_token=sell_token,
487
- buy_token=buy_token,
488
- api_keys=api_keys,
489
- web3=web3,
490
- wait_order_complete=False,
491
- )
492
482
 
493
483
  try:
484
+ _, order = swap_tokens_waiting(
485
+ amount_wei=amount_wei,
486
+ sell_token=sell_token,
487
+ buy_token=buy_token,
488
+ api_keys=api_keys,
489
+ web3=web3,
490
+ wait_order_complete=False,
491
+ timeout=timedelta(minutes=2),
492
+ )
494
493
  order_metadata = asyncio.run(wait_for_order_completion(order=order))
495
494
  logger.debug(
496
495
  f"Swapped {sell_token} for {buy_token}. Order details {order_metadata}"
497
496
  )
498
497
  return order_metadata.uid.root
499
498
 
500
- except TimeoutError:
501
- # Since timeout occurred, we need to cancel the order before trying to swap again.
502
- asyncio.run(cancel_order(order_uids=[order.uid.root], api_keys=api_keys))
503
- logger.info("TimeoutError. Trying to swap directly on Swapr pools.")
499
+ except (UnexpectedResponseError, TimeoutError) as e:
500
+ # We don't retry if not enough balance.
501
+ if "InsufficientBalance" in str(e):
502
+ raise e
503
+ # Note that we don't need to cancel the order because we are setting
504
+ # timeout and valid_to in the order, thus the order simply expires.
505
+ logger.info(
506
+ f"Exception occured when swapping tokens via Cowswap, doing swap via pools. {e}"
507
+ )
504
508
 
505
509
  tx_receipt = SwapPoolHandler(
506
510
  api_keys=api_keys,
@@ -55,7 +55,8 @@ from prediction_market_agent_tooling.markets.omen.cow_contracts import (
55
55
  )
56
56
  from prediction_market_agent_tooling.tools.contract import ContractERC20OnGnosisChain
57
57
  from prediction_market_agent_tooling.tools.cow.models import MinimalisticToken, Order
58
- from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow
58
+ from prediction_market_agent_tooling.tools.cow.semaphore import postgres_rate_limited
59
+ from prediction_market_agent_tooling.tools.utils import utcnow
59
60
 
60
61
 
61
62
  class OrderStatusError(Exception):
@@ -190,7 +191,11 @@ def handle_allowance(
190
191
  )
191
192
 
192
193
 
194
+ @postgres_rate_limited(
195
+ api_keys=APIKeys(), rate_id="swap_tokens_waiting", interval_seconds=60.0
196
+ )
193
197
  @tenacity.retry(
198
+ reraise=True,
194
199
  stop=stop_after_attempt(3),
195
200
  wait=wait_fixed(1),
196
201
  retry=tenacity.retry_if_not_exception_type((TimeoutError, OrderStatusError)),
@@ -205,6 +210,7 @@ def swap_tokens_waiting(
205
210
  env: Envs = "prod",
206
211
  web3: Web3 | None = None,
207
212
  wait_order_complete: bool = True,
213
+ timeout: timedelta = timedelta(seconds=120),
208
214
  ) -> tuple[OrderMetaData | None, CompletedOrder]:
209
215
  # CoW library uses async, so we need to wrap the call in asyncio.run for us to use it.
210
216
  return asyncio.run(
@@ -215,6 +221,7 @@ def swap_tokens_waiting(
215
221
  api_keys,
216
222
  chain,
217
223
  env,
224
+ timeout=timeout,
218
225
  web3=web3,
219
226
  wait_order_complete=wait_order_complete,
220
227
  )
@@ -229,6 +236,7 @@ async def place_swap_order(
229
236
  chain: Chain,
230
237
  env: Envs,
231
238
  slippage_tolerance: float = 0.01,
239
+ valid_to: int | None = None,
232
240
  ) -> CompletedOrder:
233
241
  account = api_keys.get_account()
234
242
  safe_address = api_keys.safe_address_checksum
@@ -242,6 +250,7 @@ async def place_swap_order(
242
250
  chain=chain,
243
251
  env=env,
244
252
  slippage_tolerance=slippage_tolerance,
253
+ valid_to=valid_to,
245
254
  )
246
255
  logger.info(f"Order created: {order}")
247
256
 
@@ -299,6 +308,7 @@ async def swap_tokens_waiting_async(
299
308
  handle_allowance(
300
309
  api_keys=api_keys, sell_token=sell_token, amount_wei=amount_wei, web3=web3
301
310
  )
311
+ valid_to = (utcnow() + timeout).timestamp()
302
312
  order = await place_swap_order(
303
313
  api_keys=api_keys,
304
314
  amount_wei=amount_wei,
@@ -307,6 +317,7 @@ async def swap_tokens_waiting_async(
307
317
  chain=chain,
308
318
  env=env,
309
319
  slippage_tolerance=slippage_tolerance,
320
+ valid_to=int(valid_to),
310
321
  )
311
322
  if wait_order_complete:
312
323
  order_metadata = await wait_for_order_completion(order=order, timeout=timeout)
@@ -327,11 +338,8 @@ async def sign_safe_cow_swap(
327
338
  ) -> None:
328
339
  order_book_api = get_order_book_api(env, chain)
329
340
  posted_order = await order_book_api.get_order_by_uid(order.uid)
330
- CowGPv2SettlementContract(
331
- address=Web3.to_checksum_address(
332
- check_not_none(posted_order.settlementContract).root
333
- )
334
- ).setPreSignature(
341
+
342
+ CowGPv2SettlementContract().setPreSignature(
335
343
  api_keys,
336
344
  HexBytes(posted_order.uid.root),
337
345
  True,
@@ -1,7 +1,9 @@
1
1
  from pydantic import BaseModel
2
+ from sqlmodel import Field, SQLModel
2
3
 
3
4
  from prediction_market_agent_tooling.gtypes import ChecksumAddress
4
5
  from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
6
+ from prediction_market_agent_tooling.tools.utils import utcnow
5
7
 
6
8
 
7
9
  class MinimalisticToken(BaseModel):
@@ -14,3 +16,9 @@ class Order(BaseModel):
14
16
  sellToken: str
15
17
  buyToken: str
16
18
  creationDate: DatetimeUTC
19
+
20
+
21
+ class RateLimit(SQLModel, table=True):
22
+ __tablename__ = "rate_limit"
23
+ id: str = Field(primary_key=True)
24
+ last_called_at: DatetimeUTC = Field(default_factory=utcnow)
@@ -0,0 +1,101 @@
1
+ import time
2
+ from datetime import timedelta
3
+ from functools import wraps
4
+ from typing import Any, Callable, Optional, TypeVar, cast
5
+
6
+ from sqlalchemy.exc import OperationalError
7
+ from sqlmodel import Session, select
8
+
9
+ from prediction_market_agent_tooling.config import APIKeys
10
+ from prediction_market_agent_tooling.tools.cow.models import RateLimit
11
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
12
+ from prediction_market_agent_tooling.tools.db.db_manager import DBManager
13
+ from prediction_market_agent_tooling.tools.utils import utcnow
14
+
15
+ F = TypeVar("F", bound=Callable[..., Any])
16
+
17
+ FALLBACK_SQL_ENGINE = "sqlite:///rate_limit.db"
18
+
19
+
20
+ def postgres_rate_limited(
21
+ api_keys: APIKeys, rate_id: str = "default", interval_seconds: float = 1.0
22
+ ) -> Callable[[F], F]:
23
+ """rate_id is used to distinguish between different rate limits for different functions"""
24
+ limiter = RateLimiter(id=rate_id, interval_seconds=interval_seconds)
25
+
26
+ def decorator(func: F) -> F:
27
+ @wraps(func)
28
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
29
+ sqlalchemy_db_url = (
30
+ api_keys.sqlalchemy_db_url.get_secret_value()
31
+ if api_keys.SQLALCHEMY_DB_URL
32
+ else FALLBACK_SQL_ENGINE
33
+ )
34
+
35
+ db_manager = DBManager(sqlalchemy_db_url)
36
+ db_manager.create_tables([RateLimit])
37
+
38
+ with db_manager.get_session() as session:
39
+ limiter.enforce(session)
40
+ return func(*args, **kwargs)
41
+
42
+ return cast(F, wrapper)
43
+
44
+ return decorator
45
+
46
+
47
+ class RateLimiter:
48
+ def __init__(self, id: str, interval_seconds: float = 1.0) -> None:
49
+ self.id = id
50
+ self.interval = timedelta(seconds=interval_seconds)
51
+
52
+ def enforce(self, session: Session, timeout_seconds: float = 30.0) -> None:
53
+ """
54
+ Enforces the rate limit inside a transaction.
55
+ Blocks until allowed or timeout is reached.
56
+
57
+ Args:
58
+ session: The database session to use
59
+ timeout_seconds: Maximum time in seconds to wait before giving up
60
+
61
+ Raises:
62
+ TimeoutError: If the rate limit cannot be acquired within the timeout period
63
+ """
64
+ start_time = time.monotonic()
65
+
66
+ while True:
67
+ try:
68
+ with session.begin():
69
+ stmt = (
70
+ select(RateLimit)
71
+ .where(RateLimit.id == self.id)
72
+ .with_for_update()
73
+ )
74
+ result: Optional[RateLimit] = session.exec(stmt).first()
75
+
76
+ now = utcnow()
77
+
78
+ if result is None:
79
+ # First time this limiter is used
80
+ session.add(RateLimit(id=self.id))
81
+ return
82
+
83
+ last_called_aware = DatetimeUTC.from_datetime(result.last_called_at)
84
+ elapsed = now - last_called_aware
85
+ if elapsed >= self.interval:
86
+ result.last_called_at = now
87
+ session.add(result)
88
+ return
89
+
90
+ # Not enough time passed, sleep and retry
91
+ to_sleep = (self.interval - elapsed).total_seconds()
92
+ time.sleep(to_sleep)
93
+ except OperationalError:
94
+ # Backoff if DB is under contention
95
+ elapsed_time = time.monotonic() - start_time
96
+ if elapsed_time > timeout_seconds:
97
+ raise TimeoutError(
98
+ f"Could not acquire rate limit '{self.id}' "
99
+ f"after {elapsed_time:.1f} seconds due to database contention"
100
+ )
101
+ time.sleep(0.5)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prediction-market-agent-tooling
3
- Version: 0.66.0.dev791
3
+ Version: 0.66.2
4
4
  Summary: Tools to benchmark, deploy and monitor prediction market agents.
5
5
  Author: Gnosis
6
6
  Requires-Python: >=3.10,<3.13
@@ -14,7 +14,7 @@ Provides-Extra: openai
14
14
  Provides-Extra: optuna
15
15
  Requires-Dist: autoflake (>=2.2.1,<3.0.0)
16
16
  Requires-Dist: base58 (>=1.0.2,<2.0)
17
- Requires-Dist: cowdao-cowpy-kongzii (==1.0.0rc2)
17
+ Requires-Dist: cowdao-cowpy (==1.0.0rc5)
18
18
  Requires-Dist: cron-validator (>=1.0.8,<2.0.0)
19
19
  Requires-Dist: eth-account (>=0.8.0,<0.12.0)
20
20
  Requires-Dist: eth-keys (>=0.6.1,<0.7.0)
@@ -26,9 +26,9 @@ prediction_market_agent_tooling/benchmark/utils.py,sha256=xQd7p9H08-OtN3iC4QT2i9
26
26
  prediction_market_agent_tooling/chains.py,sha256=1qQstoqXMwqwM7k-KH7MjMz8Ei-D83KZByvDbCZpAxs,116
27
27
  prediction_market_agent_tooling/config.py,sha256=-kJfdDr-m0R-tGZ1KRI-hJJk0mXDt142CAlvwaJ2N2I,11778
28
28
  prediction_market_agent_tooling/data_download/langfuse_data_downloader.py,sha256=VY23h324VKIVkevj1B1O-zL1eEp9AElmcfn6SwYDUSc,14246
29
- prediction_market_agent_tooling/deploy/agent.py,sha256=HLK5rf38PWgKydc_aIUck-wYzWQUnPdhto3Evoq82Lg,26282
29
+ prediction_market_agent_tooling/deploy/agent.py,sha256=EEAZQnOqsru1xRzt141iBy_9tG7zo1vnxkLqpg1SuDw,26313
30
30
  prediction_market_agent_tooling/deploy/agent_example.py,sha256=yS1fWkHynr9MYGNOM2WsCnRWLPaffY4bOc6bIudrdd4,1377
31
- prediction_market_agent_tooling/deploy/betting_strategy.py,sha256=YYayGjTKW02d3BUavJ8M3NmFk41oldEM3FHbwppZGRM,17184
31
+ prediction_market_agent_tooling/deploy/betting_strategy.py,sha256=jyBz1vIRXWxzDoC5AeO-r-V6qntaSfJ-QoHSc_I7JBM,20268
32
32
  prediction_market_agent_tooling/deploy/constants.py,sha256=Qe9cllgsGMkecfmbhXoFkPxuJyG6ATsrT87RF9SmPWM,249
33
33
  prediction_market_agent_tooling/deploy/gcp/deploy.py,sha256=CYUgnfy-9XVk04kkxA_5yp0GE9Mw5caYqlFUZQ2j3ks,3739
34
34
  prediction_market_agent_tooling/deploy/gcp/kubernetes_models.py,sha256=OsPboCFGiZKsvGyntGZHwdqPlLTthITkNF5rJFvGgU8,2582
@@ -37,14 +37,14 @@ prediction_market_agent_tooling/deploy/trade_interval.py,sha256=Xk9j45alQ_vrasGv
37
37
  prediction_market_agent_tooling/gtypes.py,sha256=bUIZfZIGvIi3aiZNu5rVE9kRevw8sfMa4bcze6QeBg8,6058
38
38
  prediction_market_agent_tooling/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  prediction_market_agent_tooling/jobs/jobs_models.py,sha256=DoZ9dlvVhpNrnINiR1uy6YUOsuzI_L-avBt362y5xXM,2467
40
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py,sha256=qbTZ9HVvu_iP4dDxuvOZxAp6JsRKejvEW2YDYCnRmd4,5039
40
+ prediction_market_agent_tooling/jobs/omen/omen_jobs.py,sha256=UN7OF-oi3_cgaGEBxJO2SxzURnuTFFEUcrUWM2rnSGA,5044
41
41
  prediction_market_agent_tooling/loggers.py,sha256=o1HyvwtK1DbuC0YWQwJNqzXLLbSC41gNBkEUxiAziEg,5796
42
42
  prediction_market_agent_tooling/logprobs_parser.py,sha256=DBlBQtWX8_URXhzTU3YWIPa76Zx3QDHlx1ARqbgJsVI,5008
43
43
  prediction_market_agent_tooling/markets/agent_market.py,sha256=nMIa6BkoUWdiz12kFDKuKXD_m6PhLzp3to1vgyj63ZQ,18834
44
44
  prediction_market_agent_tooling/markets/base_subgraph_handler.py,sha256=7RaYO_4qAmQ6ZGM8oPK2-CkiJfKmV9MxM-rJlduaecU,1971
45
45
  prediction_market_agent_tooling/markets/blockchain_utils.py,sha256=6REOt70v3vnzmtCbuRcUTdwt6htXy9nAfNkLOH3Bv1U,2987
46
46
  prediction_market_agent_tooling/markets/categorize.py,sha256=orLZlPaHgeREU66m1amxfWikeV77idV4sZDPB8NgSD0,1300
47
- prediction_market_agent_tooling/markets/data_models.py,sha256=Cua28jBom3lBuOifwQLEWb1MrKq2epnA5qognaV5VRM,8006
47
+ prediction_market_agent_tooling/markets/data_models.py,sha256=H3G-2I9QFhWrBY_KI-4BO2jMtb_f9yauzHz-zA4NI5Q,8016
48
48
  prediction_market_agent_tooling/markets/manifold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  prediction_market_agent_tooling/markets/manifold/api.py,sha256=tWnjuqvU8pcCuja2B_ynHeds1iiEFc6QWHjeSO_GSxY,7676
50
50
  prediction_market_agent_tooling/markets/manifold/data_models.py,sha256=3z1gFbPMEgCDGqeH-IK8wcvqmIHgLdZX8C2M1UQ7iDw,6740
@@ -53,13 +53,13 @@ prediction_market_agent_tooling/markets/manifold/utils.py,sha256=DCigEbpGWkXR-RZ
53
53
  prediction_market_agent_tooling/markets/market_fees.py,sha256=YeK3ynjYIguB0xf6sO5iyg9lOdW_HD4C6nbJfiGyRCU,1351
54
54
  prediction_market_agent_tooling/markets/markets.py,sha256=lIEPfPJD1Gz90pTvN2pZi51rpb969STPgQtNFCqHUJg,2667
55
55
  prediction_market_agent_tooling/markets/metaculus/api.py,sha256=4TRPGytQQbSdf42DCg2M_JWYPAuNjqZ3eBqaQBLkNks,2736
56
- prediction_market_agent_tooling/markets/metaculus/data_models.py,sha256=FaBCTPPezXbBwZ9p791CiVgQ4vB696xnMbz9XVXmiVI,3267
56
+ prediction_market_agent_tooling/markets/metaculus/data_models.py,sha256=WjPt0MKeJNtoY-8oLQTLC8vQYYQ-dBj8UZoPq-UBYsQ,3288
57
57
  prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=S1Kkf_F3KuqFO938d5bun60wd8lsGvnGqxsW-tgEBgw,5110
58
58
  prediction_market_agent_tooling/markets/omen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- prediction_market_agent_tooling/markets/omen/cow_contracts.py,sha256=sl1L4cK5nAJwZ2wdhLzqh8p7h_IEValNvLwKUlInKxw,957
60
- prediction_market_agent_tooling/markets/omen/data_models.py,sha256=-EhjQRQLxI0O3m4eOKUMH10_RS4tOBwIvMSnbIIHu10,31056
61
- prediction_market_agent_tooling/markets/omen/omen.py,sha256=hTHqS_-Z7jocrwuisqymYWLUUn8bK_WSRwaKPgXz1es,50477
62
- prediction_market_agent_tooling/markets/omen/omen_constants.py,sha256=pYcdK5a1nPte9x7E154vkIiM6cLw1JvqpLibs9HsBbo,347
59
+ prediction_market_agent_tooling/markets/omen/cow_contracts.py,sha256=sFaW82u_haL4nze8fjTmnQsOuV0OecunQlAhh1OAw0w,1091
60
+ prediction_market_agent_tooling/markets/omen/data_models.py,sha256=RsBYSbM4deA6Os4kQ3egH3HvwT80tQho6T1yyoATCMs,31103
61
+ prediction_market_agent_tooling/markets/omen/omen.py,sha256=fYDP64-gL3n18K86BXMrM9nO8ZQ4FQY8LynVL7Xm588,50468
62
+ prediction_market_agent_tooling/markets/omen/omen_constants.py,sha256=XtRk4vpxwUYkTndfjlcmghA-NOIneV8zdHFdyI7tHhM,487
63
63
  prediction_market_agent_tooling/markets/omen/omen_contracts.py,sha256=27-HRngTqfk_wgvttB3GeVHhy_O2YZcz9izo9OufOI0,29991
64
64
  prediction_market_agent_tooling/markets/omen/omen_resolving.py,sha256=D-ubf_LumHs_c5rBAAntQ8wGKprtO2V1JZeedmChNIE,11035
65
65
  prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py,sha256=h-YFRLY5rlhM9RqqceyfbHlno3elltN8Nr_Mnu1kJ90,40006
@@ -71,7 +71,7 @@ prediction_market_agent_tooling/markets/polymarket/utils.py,sha256=8kTeVjXPcXC6D
71
71
  prediction_market_agent_tooling/markets/seer/data_models.py,sha256=osM9WaLsxQf-pfVGq0O-IkM93ehP9a7fVUf-hi2VlMs,5523
72
72
  prediction_market_agent_tooling/markets/seer/exceptions.py,sha256=cEObdjluivD94tgOLzmimR7wgQEOt6SRakrYdhsRQtk,112
73
73
  prediction_market_agent_tooling/markets/seer/price_manager.py,sha256=MClY2NGwOV70nZYIcmzXFy6Ogd8NBIq7telQcQ3VcU4,6243
74
- prediction_market_agent_tooling/markets/seer/seer.py,sha256=vXSJ-iqkO44q4HVsKbu_6gD8lpx7t70Rbx-Or6xOao8,24354
74
+ prediction_market_agent_tooling/markets/seer/seer.py,sha256=_9NAisDsk9cu9RP3O-cphiefENN2nPK1Puk0C5eiGWg,24630
75
75
  prediction_market_agent_tooling/markets/seer/seer_contracts.py,sha256=uMzpHpI6_tgfhWxPzupLdUJlZ1P2wr0rRiYjAGClKgU,4984
76
76
  prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py,sha256=pJxch9_u0EdiIatQP1-UFClt8UEfMZAXBlk5wDO_ovk,9940
77
77
  prediction_market_agent_tooling/markets/seer/subgraph_data_models.py,sha256=0izxS8Mtzonfdl9UqvFVXrdj0hVzieroekXhogfZKCw,1817
@@ -87,8 +87,9 @@ prediction_market_agent_tooling/tools/caches/inmemory_cache.py,sha256=ZW5iI5rmjq
87
87
  prediction_market_agent_tooling/tools/caches/serializers.py,sha256=vFDx4fsPxclXp2q0sv27j4al_M_Tj9aR2JJP-xNHQXA,2151
88
88
  prediction_market_agent_tooling/tools/contract.py,sha256=pdr9ZYmj4QVUfgVKdvOU6ucYdBpJGdha_FMR_LgtcEs,22912
89
89
  prediction_market_agent_tooling/tools/costs.py,sha256=EaAJ7v9laD4VEV3d8B44M4u3_oEO_H16jRVCdoZ93Uw,954
90
- prediction_market_agent_tooling/tools/cow/cow_order.py,sha256=yTUG6VPVe292R_5IG8F90kfZfAeun8G1TL6mHsnaXM0,12984
91
- prediction_market_agent_tooling/tools/cow/models.py,sha256=ZWGW0og5vXjgZVXxkx4uV2m61Q8AmL-L-D_D4f7ApR0,379
90
+ prediction_market_agent_tooling/tools/cow/cow_order.py,sha256=cKjyVbBblOkVjBbLyJ8w7qnzkxRhawtQYeNb4rymZwc,13272
91
+ prediction_market_agent_tooling/tools/cow/models.py,sha256=jAITIcg8GXEGtRQpezVvlqaQ5M8OEiiHqLwoyUeX3JM,655
92
+ prediction_market_agent_tooling/tools/cow/semaphore.py,sha256=IJGKRgvXnRSkEt_z1i5-eFIoVMcyhr7HK0JfIz1MXdQ,3738
92
93
  prediction_market_agent_tooling/tools/custom_exceptions.py,sha256=Fh8z1fbwONvP4-j7AmV_PuEcoqb6-QXa9PJ9m7guMcM,93
93
94
  prediction_market_agent_tooling/tools/datetime_utc.py,sha256=8_WackjtjC8zHXrhQFTGQ6e6Fz_6llWoKR4CSFvIv9I,2766
94
95
  prediction_market_agent_tooling/tools/db/db_manager.py,sha256=GtzHH1NLl8HwqC8Z7s6eTlIQXuV0blxfaV2PeQrBnfQ,3013
@@ -124,8 +125,8 @@ prediction_market_agent_tooling/tools/tokens/usd.py,sha256=yuW8iPPtcpP4eLH2nORMD
124
125
  prediction_market_agent_tooling/tools/transaction_cache.py,sha256=K5YKNL2_tR10Iw2TD9fuP-CTGpBbZtNdgbd0B_R7pjg,1814
125
126
  prediction_market_agent_tooling/tools/utils.py,sha256=RlWSlzS2LavMIWrpwn1fevbzgPZruD4VcXTa-XxjWnE,7343
126
127
  prediction_market_agent_tooling/tools/web3_utils.py,sha256=0r26snqCXGdLKCWA8jpe7DV8x2NPYWZwOy4oyKyDCYk,12615
127
- prediction_market_agent_tooling-0.66.0.dev791.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
128
- prediction_market_agent_tooling-0.66.0.dev791.dist-info/METADATA,sha256=UAUwDR0E0lNWDUcntavNgEX5gQEWBo5Jpu3kF0lA3xo,8741
129
- prediction_market_agent_tooling-0.66.0.dev791.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
130
- prediction_market_agent_tooling-0.66.0.dev791.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
131
- prediction_market_agent_tooling-0.66.0.dev791.dist-info/RECORD,,
128
+ prediction_market_agent_tooling-0.66.2.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
129
+ prediction_market_agent_tooling-0.66.2.dist-info/METADATA,sha256=LrUMj4jSVZ9kX_y8LfK8xuGtbNElU3PnpqbJDPSjs_8,8726
130
+ prediction_market_agent_tooling-0.66.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
131
+ prediction_market_agent_tooling-0.66.2.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
132
+ prediction_market_agent_tooling-0.66.2.dist-info/RECORD,,