prediction-market-agent-tooling 0.61.1.dev489__py3-none-any.whl → 0.62.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 (50) hide show
  1. prediction_market_agent_tooling/deploy/agent.py +4 -5
  2. prediction_market_agent_tooling/deploy/betting_strategy.py +53 -69
  3. prediction_market_agent_tooling/gtypes.py +105 -27
  4. prediction_market_agent_tooling/jobs/jobs_models.py +5 -7
  5. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +13 -17
  6. prediction_market_agent_tooling/markets/agent_market.py +96 -55
  7. prediction_market_agent_tooling/markets/blockchain_utils.py +2 -32
  8. prediction_market_agent_tooling/markets/data_models.py +40 -44
  9. prediction_market_agent_tooling/markets/manifold/api.py +2 -6
  10. prediction_market_agent_tooling/markets/manifold/data_models.py +33 -25
  11. prediction_market_agent_tooling/markets/manifold/manifold.py +13 -11
  12. prediction_market_agent_tooling/markets/market_fees.py +6 -2
  13. prediction_market_agent_tooling/markets/omen/data_models.py +66 -57
  14. prediction_market_agent_tooling/markets/omen/omen.py +222 -250
  15. prediction_market_agent_tooling/markets/omen/omen_contracts.py +32 -33
  16. prediction_market_agent_tooling/markets/omen/omen_resolving.py +7 -14
  17. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +20 -14
  18. prediction_market_agent_tooling/markets/polymarket/data_models.py +3 -3
  19. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +4 -4
  20. prediction_market_agent_tooling/markets/polymarket/polymarket.py +3 -5
  21. prediction_market_agent_tooling/markets/seer/data_models.py +92 -5
  22. prediction_market_agent_tooling/markets/seer/seer.py +93 -115
  23. prediction_market_agent_tooling/markets/seer/seer_contracts.py +11 -6
  24. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +18 -26
  25. prediction_market_agent_tooling/monitor/monitor.py +2 -2
  26. prediction_market_agent_tooling/tools/_generic_value.py +261 -0
  27. prediction_market_agent_tooling/tools/balances.py +14 -11
  28. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +12 -10
  29. prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +31 -24
  30. prediction_market_agent_tooling/tools/betting_strategies/utils.py +3 -1
  31. prediction_market_agent_tooling/tools/contract.py +14 -10
  32. prediction_market_agent_tooling/tools/cow/cow_manager.py +3 -4
  33. prediction_market_agent_tooling/tools/cow/cow_order.py +51 -7
  34. prediction_market_agent_tooling/tools/langfuse_client_utils.py +13 -1
  35. prediction_market_agent_tooling/tools/omen/sell_positions.py +6 -3
  36. prediction_market_agent_tooling/tools/safe.py +5 -6
  37. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +36 -27
  38. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +4 -25
  39. prediction_market_agent_tooling/tools/tokens/main_token.py +2 -2
  40. prediction_market_agent_tooling/tools/tokens/token_utils.py +46 -0
  41. prediction_market_agent_tooling/tools/tokens/usd.py +79 -0
  42. prediction_market_agent_tooling/tools/utils.py +14 -8
  43. prediction_market_agent_tooling/tools/web3_utils.py +24 -41
  44. {prediction_market_agent_tooling-0.61.1.dev489.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/METADATA +2 -1
  45. {prediction_market_agent_tooling-0.61.1.dev489.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/RECORD +48 -47
  46. prediction_market_agent_tooling/markets/seer/price_manager.py +0 -111
  47. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +0 -57
  48. {prediction_market_agent_tooling-0.61.1.dev489.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/LICENSE +0 -0
  49. {prediction_market_agent_tooling-0.61.1.dev489.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/WHEEL +0 -0
  50. {prediction_market_agent_tooling-0.61.1.dev489.dist-info → prediction_market_agent_tooling-0.62.0.dist-info}/entry_points.txt +0 -0
@@ -14,6 +14,8 @@ from cowdao_cowpy.order_book.generated.model import (
14
14
  OrderMetaData,
15
15
  OrderQuoteRequest,
16
16
  OrderQuoteSide1,
17
+ OrderQuoteSide3,
18
+ OrderQuoteSideKindBuy,
17
19
  OrderQuoteSideKindSell,
18
20
  OrderStatus,
19
21
  TokenAmount,
@@ -21,27 +23,63 @@ from cowdao_cowpy.order_book.generated.model import (
21
23
  from eth_account.signers.local import LocalAccount
22
24
  from eth_typing.evm import ChecksumAddress
23
25
  from web3 import Web3
24
- from web3.types import Wei
25
26
 
26
27
  from prediction_market_agent_tooling.config import APIKeys
27
- from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei, wei_type
28
+ from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei
28
29
  from prediction_market_agent_tooling.loggers import logger
29
30
  from prediction_market_agent_tooling.tools.contract import ContractERC20OnGnosisChain
30
31
  from prediction_market_agent_tooling.tools.utils import utcnow
31
32
 
32
33
 
34
+ class OrderStatusError(Exception):
35
+ pass
36
+
37
+
33
38
  def get_order_book_api(env: Envs, chain: Chain) -> OrderBookApi:
34
39
  chain_id = SupportedChainId(chain.value[0])
35
40
  return OrderBookApi(OrderBookAPIConfigFactory.get_config(env, chain_id))
36
41
 
37
42
 
43
+ @tenacity.retry(
44
+ stop=tenacity.stop_after_attempt(3),
45
+ wait=tenacity.wait_fixed(1),
46
+ after=lambda x: logger.debug(f"get_sell_token_amount failed, {x.attempt_number=}."),
47
+ )
48
+ def get_sell_token_amount(
49
+ buy_amount: Wei,
50
+ sell_token: ChecksumAddress,
51
+ buy_token: ChecksumAddress,
52
+ chain: Chain = Chain.GNOSIS,
53
+ env: Envs = "prod",
54
+ ) -> Wei:
55
+ """
56
+ Calculate how much of the sell_token is needed to obtain a specified amount of buy_token.
57
+ """
58
+ order_book_api = get_order_book_api(env, chain)
59
+ order_quote_request = OrderQuoteRequest(
60
+ sellToken=Address(sell_token),
61
+ buyToken=Address(buy_token),
62
+ from_=Address(
63
+ "0x1234567890abcdef1234567890abcdef12345678"
64
+ ), # Just random address, doesn't matter.
65
+ )
66
+ order_side = OrderQuoteSide3(
67
+ kind=OrderQuoteSideKindBuy.buy,
68
+ buyAmountAfterFee=TokenAmount(str(buy_amount)),
69
+ )
70
+ order_quote = asyncio.run(
71
+ order_book_api.post_quote(order_quote_request, order_side)
72
+ )
73
+ return Wei(order_quote.quote.sellAmount.root)
74
+
75
+
38
76
  @tenacity.retry(
39
77
  stop=tenacity.stop_after_attempt(3),
40
78
  wait=tenacity.wait_fixed(1),
41
79
  after=lambda x: logger.debug(f"get_buy_token_amount failed, {x.attempt_number=}."),
42
80
  )
43
81
  def get_buy_token_amount(
44
- amount_wei: Wei,
82
+ sell_amount: Wei,
45
83
  sell_token: ChecksumAddress,
46
84
  buy_token: ChecksumAddress,
47
85
  chain: Chain = Chain.GNOSIS,
@@ -57,14 +95,20 @@ def get_buy_token_amount(
57
95
  )
58
96
  order_side = OrderQuoteSide1(
59
97
  kind=OrderQuoteSideKindSell.sell,
60
- sellAmountBeforeFee=TokenAmount(str(amount_wei)),
98
+ sellAmountBeforeFee=TokenAmount(str(sell_amount)),
61
99
  )
62
100
  order_quote = asyncio.run(
63
101
  order_book_api.post_quote(order_quote_request, order_side)
64
102
  )
65
- return wei_type(order_quote.quote.buyAmount.root)
103
+ return Wei(order_quote.quote.buyAmount.root)
66
104
 
67
105
 
106
+ @tenacity.retry(
107
+ stop=tenacity.stop_after_attempt(3),
108
+ wait=tenacity.wait_fixed(1),
109
+ retry=tenacity.retry_if_not_exception_type((TimeoutError, OrderStatusError)),
110
+ after=lambda x: logger.debug(f"swap_tokens_waiting failed, {x.attempt_number=}."),
111
+ )
68
112
  def swap_tokens_waiting(
69
113
  amount_wei: Wei,
70
114
  sell_token: ChecksumAddress,
@@ -102,7 +146,7 @@ async def swap_tokens_waiting_async(
102
146
  timeout: timedelta = timedelta(seconds=60),
103
147
  ) -> OrderMetaData:
104
148
  order = await swap_tokens(
105
- amount=amount_wei,
149
+ amount=amount_wei.value,
106
150
  sell_token=sell_token,
107
151
  buy_token=buy_token,
108
152
  account=account,
@@ -125,7 +169,7 @@ async def swap_tokens_waiting_async(
125
169
  OrderStatus.cancelled,
126
170
  OrderStatus.expired,
127
171
  ):
128
- raise ValueError(f"Order {order.uid} failed. {order.url}")
172
+ raise OrderStatusError(f"Order {order.uid} failed. {order.url}")
129
173
 
130
174
  if utcnow() - start_time > timeout:
131
175
  raise TimeoutError(
@@ -13,6 +13,9 @@ from prediction_market_agent_tooling.markets.data_models import (
13
13
  TradeType,
14
14
  )
15
15
  from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
16
+ from prediction_market_agent_tooling.markets.omen.omen_constants import (
17
+ WRAPPED_XDAI_CONTRACT_ADDRESS,
18
+ )
16
19
  from prediction_market_agent_tooling.tools.utils import DatetimeUTC
17
20
 
18
21
 
@@ -147,11 +150,20 @@ def get_trace_for_bet(
147
150
  # Filter for traces with the same bet outcome and amount
148
151
  traces_for_bet: list[ProcessMarketTrace] = []
149
152
  for t in traces:
153
+ if (
154
+ t.market.collateral_token_contract_address_checksummed
155
+ not in WRAPPED_XDAI_CONTRACT_ADDRESS
156
+ ):
157
+ # 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
+ continue
150
162
  # Cannot use exact comparison due to gas fees
151
163
  if (
152
164
  t.buy_trade
153
165
  and t.buy_trade.outcome == bet.outcome
154
- and np.isclose(t.buy_trade.amount.amount, bet.amount.amount)
166
+ and np.isclose(t.buy_trade.amount.value, bet.amount.value)
155
167
  ):
156
168
  traces_for_bet.append(t)
157
169
 
@@ -1,6 +1,7 @@
1
1
  from datetime import timedelta
2
2
 
3
3
  from prediction_market_agent_tooling.config import APIKeys
4
+ from prediction_market_agent_tooling.gtypes import USD
4
5
  from prediction_market_agent_tooling.loggers import logger
5
6
  from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
6
7
  from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
@@ -23,7 +24,9 @@ def sell_all(
23
24
  better_address=better_address,
24
25
  market_opening_after=utcnow() + timedelta(days=closing_later_than_days),
25
26
  )
26
- bets_total_usd = sum(b.collateral_amount_usd for b in bets)
27
+ bets_total_usd = sum(
28
+ (b.get_collateral_amount_usd() for b in bets), start=USD.zero()
29
+ )
27
30
  unique_market_urls = set(b.fpmm.url for b in bets)
28
31
  starting_balance = get_balances(better_address)
29
32
  new_balance = starting_balance
@@ -37,9 +40,9 @@ def sell_all(
37
40
  outcome = agent_market.outcomes[bet.outcomeIndex]
38
41
  current_token_balance = agent_market.get_token_balance(better_address, outcome)
39
42
 
40
- if current_token_balance.amount <= OmenAgentMarket.get_tiny_bet_amount().amount:
43
+ if current_token_balance.as_token <= agent_market.get_tiny_bet_amount():
41
44
  logger.info(
42
- f"Skipping bet on {bet.fpmm.url} because the actual balance is unreasonably low {current_token_balance.amount}."
45
+ f"Skipping bet on {bet.fpmm.url} because the actual balance is unreasonably low {current_token_balance}."
43
46
  )
44
47
  continue
45
48
 
@@ -11,11 +11,10 @@ from safe_eth.eth.constants import NULL_ADDRESS
11
11
  from safe_eth.eth.contracts import get_safe_V1_4_1_contract
12
12
  from safe_eth.safe.proxy_factory import ProxyFactoryV141
13
13
  from safe_eth.safe.safe import SafeV141
14
- from web3.types import Wei
15
14
 
15
+ from prediction_market_agent_tooling.gtypes import Wei
16
16
  from prediction_market_agent_tooling.loggers import logger
17
17
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
18
- from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai
19
18
 
20
19
 
21
20
  def create_safe(
@@ -63,10 +62,10 @@ def create_safe(
63
62
  f"does not exist on network {ethereum_network.name}"
64
63
  )
65
64
 
66
- account_balance = ethereum_client.get_balance(account.address)
67
- account_balance_xdai = wei_to_xdai(account_balance)
65
+ account_balance = Wei(ethereum_client.get_balance(account.address))
66
+ account_balance_xdai = account_balance.as_token
68
67
  # We set a reasonable expected balance below for Safe deployment not to fail.
69
- if account_balance_xdai < 0.01:
68
+ if account_balance_xdai.value < 0.01:
70
69
  raise ValueError(
71
70
  f"Client's balance is {account_balance_xdai} xDAI, too low for deploying a Safe."
72
71
  )
@@ -108,7 +107,7 @@ def create_safe(
108
107
  payment_token,
109
108
  payment,
110
109
  payment_receiver,
111
- ).build_transaction({"gas": 1, "gasPrice": Wei(1)})["data"]
110
+ ).build_transaction({"gas": 1, "gasPrice": Wei(1).value})["data"]
112
111
  )
113
112
 
114
113
  proxy_factory = ProxyFactoryV141(proxy_factory_address, ethereum_client)
@@ -1,7 +1,7 @@
1
1
  from web3 import Web3
2
2
 
3
3
  from prediction_market_agent_tooling.config import APIKeys
4
- from prediction_market_agent_tooling.gtypes import Wei, wei_type
4
+ from prediction_market_agent_tooling.gtypes import USD, Wei
5
5
  from prediction_market_agent_tooling.loggers import logger
6
6
  from prediction_market_agent_tooling.tools.contract import (
7
7
  ContractDepositableWrapperERC20BaseClass,
@@ -10,31 +10,46 @@ from prediction_market_agent_tooling.tools.contract import (
10
10
  ContractERC4626BaseClass,
11
11
  )
12
12
  from prediction_market_agent_tooling.tools.cow.cow_order import (
13
- get_buy_token_amount,
13
+ get_sell_token_amount,
14
14
  swap_tokens_waiting,
15
15
  )
16
16
  from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
17
+ from prediction_market_agent_tooling.tools.tokens.usd import get_usd_in_token
17
18
  from prediction_market_agent_tooling.tools.utils import should_not_happen
18
- from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai
19
19
 
20
20
 
21
21
  def auto_deposit_collateral_token(
22
22
  collateral_token_contract: ContractERC20BaseClass,
23
- amount_wei: Wei,
23
+ collateral_amount_wei_or_usd: Wei | USD,
24
24
  api_keys: APIKeys,
25
25
  web3: Web3 | None = None,
26
+ surplus: float = 0.01,
26
27
  ) -> None:
28
+ collateral_amount_wei = (
29
+ collateral_amount_wei_or_usd
30
+ if isinstance(collateral_amount_wei_or_usd, Wei)
31
+ else get_usd_in_token(
32
+ collateral_amount_wei_or_usd, collateral_token_contract.address
33
+ ).as_wei
34
+ )
35
+ # Deposit a bit more to cover any small changes in conversions.
36
+ collateral_amount_wei = collateral_amount_wei.with_fraction(surplus)
37
+
27
38
  if isinstance(collateral_token_contract, ContractDepositableWrapperERC20BaseClass):
28
39
  # In this case, we can use deposit function directly, no need to go through DEX.
29
40
  auto_deposit_depositable_wrapper_erc20(
30
- collateral_token_contract, amount_wei, api_keys, web3
41
+ collateral_token_contract, collateral_amount_wei, api_keys, web3
31
42
  )
32
43
 
33
44
  elif isinstance(collateral_token_contract, ContractERC4626BaseClass):
34
- auto_deposit_erc4626(collateral_token_contract, amount_wei, api_keys, web3)
45
+ auto_deposit_erc4626(
46
+ collateral_token_contract, collateral_amount_wei, api_keys, web3
47
+ )
35
48
 
36
49
  elif isinstance(collateral_token_contract, ContractERC20BaseClass):
37
- auto_deposit_erc20(collateral_token_contract, amount_wei, api_keys, web3)
50
+ auto_deposit_erc20(
51
+ collateral_token_contract, collateral_amount_wei, api_keys, web3
52
+ )
38
53
 
39
54
  else:
40
55
  should_not_happen("Unsupported ERC20 contract type.")
@@ -55,16 +70,16 @@ def auto_deposit_depositable_wrapper_erc20(
55
70
  return
56
71
 
57
72
  # If we don't have enough, we need to deposit the difference.
58
- left_to_deposit = Wei(amount_wei - collateral_token_balance)
73
+ left_to_deposit = amount_wei - collateral_token_balance
59
74
  logger.info(
60
- f"Depositing {wei_to_xdai(left_to_deposit)} {collateral_token_contract.symbol()}."
75
+ f"Depositing {left_to_deposit.as_token} {collateral_token_contract.symbol()}."
61
76
  )
62
77
  collateral_token_contract.deposit(api_keys, left_to_deposit, web3=web3)
63
78
 
64
79
 
65
80
  def auto_deposit_erc4626(
66
81
  collateral_token_contract: ContractERC4626BaseClass,
67
- asset_amount_wei: Wei,
82
+ collateral_amount_wei: Wei,
68
83
  api_keys: APIKeys,
69
84
  web3: Web3 | None,
70
85
  ) -> None:
@@ -72,12 +87,12 @@ def auto_deposit_erc4626(
72
87
  collateral_token_balance_in_shares = collateral_token_contract.balanceOf(
73
88
  for_address=for_address, web3=web3
74
89
  )
75
- asset_amount_wei_in_shares = collateral_token_contract.convertToShares(
76
- asset_amount_wei, web3
90
+ asset_amount_wei = collateral_token_contract.convertToAssets(
91
+ collateral_amount_wei, web3
77
92
  )
78
93
 
79
94
  # If we have enough shares, we don't need to deposit.
80
- if collateral_token_balance_in_shares >= asset_amount_wei_in_shares:
95
+ if collateral_token_balance_in_shares >= collateral_amount_wei:
81
96
  return
82
97
 
83
98
  # If we need to deposit into erc4626, we first need to have enough of the asset token.
@@ -89,7 +104,7 @@ def auto_deposit_erc4626(
89
104
  collateral_token_balance_in_assets = collateral_token_contract.convertToAssets(
90
105
  collateral_token_balance_in_shares, web3
91
106
  )
92
- left_to_deposit = Wei(asset_amount_wei - collateral_token_balance_in_assets)
107
+ left_to_deposit = asset_amount_wei - collateral_token_balance_in_assets
93
108
  if (
94
109
  collateral_token_contract.get_asset_token_balance(for_address, web3)
95
110
  < left_to_deposit
@@ -108,31 +123,25 @@ def auto_deposit_erc4626(
108
123
 
109
124
  def auto_deposit_erc20(
110
125
  collateral_token_contract: ContractERC20BaseClass,
111
- amount_xdai_wei: Wei,
126
+ collateral_amount_wei: Wei,
112
127
  api_keys: APIKeys,
113
128
  web3: Web3 | None,
114
129
  ) -> None:
115
- # How much it is in the other token (collateral token).
116
- collateral_amount_wei = get_buy_token_amount(
117
- amount_xdai_wei,
118
- KEEPING_ERC20_TOKEN.address,
119
- collateral_token_contract.address,
120
- )
121
130
  # How much do we have already in the other token (collateral token).
122
131
  collateral_balance_wei = collateral_token_contract.balanceOf(
123
132
  api_keys.bet_from_address
124
133
  )
125
134
  # Amount of collateral token remaining to get.
126
135
  remaining_to_get_in_collateral_wei = max(
127
- 0, collateral_amount_wei - collateral_balance_wei
136
+ Wei(0), collateral_amount_wei - collateral_balance_wei
128
137
  )
129
138
  if not remaining_to_get_in_collateral_wei:
130
139
  return
131
- # Estimate of how much of the source token we need to sell in order to fill the remaining collateral amount, with 1% slippage to be sure.
132
- amount_to_sell_wei = wei_type(
133
- (remaining_to_get_in_collateral_wei * amount_xdai_wei)
134
- / collateral_amount_wei
135
- * 1.01
140
+ # Get of how much of the source token we need to sell in order to fill the remaining collateral amount.
141
+ amount_to_sell_wei = get_sell_token_amount(
142
+ remaining_to_get_in_collateral_wei,
143
+ sell_token=KEEPING_ERC20_TOKEN.address,
144
+ buy_token=collateral_token_contract.address,
136
145
  )
137
146
  # If we don't have enough of the source token.
138
147
  if amount_to_sell_wei > ContractERC20OnGnosisChain(
@@ -7,16 +7,9 @@ from prediction_market_agent_tooling.tools.contract import (
7
7
  ContractERC20BaseClass,
8
8
  ContractERC4626BaseClass,
9
9
  )
10
- from prediction_market_agent_tooling.tools.cow.cow_order import (
11
- get_buy_token_amount,
12
- swap_tokens_waiting,
13
- )
10
+ from prediction_market_agent_tooling.tools.cow.cow_order import swap_tokens_waiting
14
11
  from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
15
12
  from prediction_market_agent_tooling.tools.utils import should_not_happen
16
- from prediction_market_agent_tooling.tools.web3_utils import (
17
- remove_fraction,
18
- wei_to_xdai,
19
- )
20
13
 
21
14
 
22
15
  def auto_withdraw_collateral_token(
@@ -24,14 +17,7 @@ def auto_withdraw_collateral_token(
24
17
  amount_wei: Wei,
25
18
  api_keys: APIKeys,
26
19
  web3: Web3 | None = None,
27
- slippage: float = 0.001,
28
20
  ) -> None:
29
- # Small slippage as exact exchange rate constantly changes and we don't care about small differences.
30
- amount_wei = remove_fraction(
31
- amount_wei,
32
- slippage,
33
- )
34
-
35
21
  if not amount_wei:
36
22
  logger.warning(
37
23
  f"Amount to withdraw is zero, skipping withdrawal of {collateral_token_contract.symbol_cached(web3)}."
@@ -51,7 +37,7 @@ def auto_withdraw_collateral_token(
51
37
  ):
52
38
  # If the ERC4626 is backed by KEEPING_ERC20_TOKEN, we can withdraw it directly, no need to go through DEX.
53
39
  logger.info(
54
- f"Withdrawing {wei_to_xdai(amount_wei)} from {collateral_token_contract.symbol_cached(web3)} into {KEEPING_ERC20_TOKEN.symbol_cached(web3)}"
40
+ f"Withdrawing {amount_wei.as_token} from {collateral_token_contract.symbol_cached(web3)} into {KEEPING_ERC20_TOKEN.symbol_cached(web3)}"
55
41
  )
56
42
  collateral_token_contract.withdraw_in_shares(
57
43
  api_keys,
@@ -60,18 +46,11 @@ def auto_withdraw_collateral_token(
60
46
  )
61
47
  elif isinstance(collateral_token_contract, ContractERC20BaseClass):
62
48
  logger.info(
63
- f"Swapping {wei_to_xdai(amount_wei)} {collateral_token_contract.symbol_cached(web3)} into {KEEPING_ERC20_TOKEN.symbol_cached(web3)}"
49
+ f"Swapping {amount_wei.as_token} from {collateral_token_contract.symbol_cached(web3)} into {KEEPING_ERC20_TOKEN.symbol_cached(web3)}"
64
50
  )
65
51
  # Otherwise, DEX will handle the rest of token swaps.
66
- # First, convert `amount_wei` from xDai-based value into the collateral token-based value.
67
- collateral_amount_wei = get_buy_token_amount(
68
- amount_wei,
69
- KEEPING_ERC20_TOKEN.address,
70
- collateral_token_contract.address,
71
- )
72
- # And then sell it.
73
52
  swap_tokens_waiting(
74
- amount_wei=collateral_amount_wei,
53
+ amount_wei=amount_wei,
75
54
  sell_token=collateral_token_contract.address,
76
55
  buy_token=KEEPING_ERC20_TOKEN.address,
77
56
  api_keys=api_keys,
@@ -1,4 +1,4 @@
1
- from prediction_market_agent_tooling.gtypes import xdai_type
1
+ from prediction_market_agent_tooling.gtypes import xDai
2
2
  from prediction_market_agent_tooling.markets.omen.omen_constants import (
3
3
  WRAPPED_XDAI_CONTRACT_ADDRESS,
4
4
  )
@@ -15,4 +15,4 @@ KEEPING_ERC20_TOKEN = ContractDepositableWrapperERC20OnGnosisChain(
15
15
  address=WRAPPED_XDAI_CONTRACT_ADDRESS
16
16
  )
17
17
 
18
- MINIMUM_NATIVE_TOKEN_IN_EOA_FOR_FEES = xdai_type(0.1)
18
+ MINIMUM_NATIVE_TOKEN_IN_EOA_FOR_FEES = xDai(0.1)
@@ -0,0 +1,46 @@
1
+ from eth_typing.evm import ChecksumAddress
2
+ from web3 import Web3
3
+
4
+ from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei
5
+ from prediction_market_agent_tooling.tools.contract import (
6
+ ContractERC4626BaseClass,
7
+ init_collateral_token_contract,
8
+ to_gnosis_chain_contract,
9
+ )
10
+ from prediction_market_agent_tooling.tools.cow.cow_order import get_buy_token_amount
11
+
12
+
13
+ def convert_to_another_token(
14
+ amount: Wei,
15
+ from_token: ChecksumAddress,
16
+ to_token: ChecksumAddress,
17
+ web3: Web3 | None = None,
18
+ ) -> Wei:
19
+ from_token_contract = to_gnosis_chain_contract(
20
+ init_collateral_token_contract(from_token, web3)
21
+ )
22
+ to_token_contract = to_gnosis_chain_contract(
23
+ init_collateral_token_contract(to_token, web3)
24
+ )
25
+
26
+ if from_token == to_token:
27
+ return amount
28
+
29
+ elif (
30
+ isinstance(to_token_contract, ContractERC4626BaseClass)
31
+ and to_token_contract.get_asset_token_contract().address == from_token
32
+ ):
33
+ return to_token_contract.convertToShares(amount)
34
+
35
+ elif (
36
+ isinstance(from_token_contract, ContractERC4626BaseClass)
37
+ and from_token_contract.get_asset_token_contract().address == to_token
38
+ ):
39
+ return from_token_contract.convertToAssets(amount)
40
+
41
+ else:
42
+ return get_buy_token_amount(
43
+ amount,
44
+ from_token,
45
+ to_token,
46
+ )
@@ -0,0 +1,79 @@
1
+ from cachetools import TTLCache, cached
2
+ from eth_typing.evm import ChecksumAddress
3
+
4
+ from prediction_market_agent_tooling.gtypes import (
5
+ USD,
6
+ ChecksumAddress,
7
+ CollateralToken,
8
+ xDai,
9
+ )
10
+ from prediction_market_agent_tooling.markets.omen.omen_constants import (
11
+ SDAI_CONTRACT_ADDRESS,
12
+ WRAPPED_XDAI_CONTRACT_ADDRESS,
13
+ )
14
+ from prediction_market_agent_tooling.tools.contract import ContractERC4626OnGnosisChain
15
+ from prediction_market_agent_tooling.tools.cow.cow_order import get_buy_token_amount
16
+
17
+
18
+ def get_usd_in_xdai(amount: USD) -> xDai:
19
+ # xDai is stable coin against USD, so for simplicity we just cast it.
20
+ return xDai(amount.value)
21
+
22
+
23
+ def get_xdai_in_usd(amount: xDai) -> USD:
24
+ # xDai is stable coin against USD, so for simplicity we just cast it.
25
+ return USD(amount.value)
26
+
27
+
28
+ def get_usd_in_token(amount: USD, token_address: ChecksumAddress) -> CollateralToken:
29
+ rate = get_single_usd_to_token_rate(token_address)
30
+ return CollateralToken(amount.value * rate.value)
31
+
32
+
33
+ def get_token_in_usd(amount: CollateralToken, token_address: ChecksumAddress) -> USD:
34
+ rate = get_single_token_to_usd_rate(token_address)
35
+ return USD(amount.value * rate.value)
36
+
37
+
38
+ # A short cache to not spam CoW and prevent timeouts, but still have relatively fresh data.
39
+ @cached(TTLCache(maxsize=100, ttl=5 * 60))
40
+ def get_single_token_to_usd_rate(token_address: ChecksumAddress) -> USD:
41
+ # (w)xDai is a stable coin against USD, so use it to estimate USD worth.
42
+ if WRAPPED_XDAI_CONTRACT_ADDRESS == token_address:
43
+ return USD(1.0)
44
+ # sDai is ERC4626 with wxDai as asset, we can take the rate directly from there instead of calling CoW.
45
+ if SDAI_CONTRACT_ADDRESS == token_address:
46
+ return USD(
47
+ ContractERC4626OnGnosisChain(address=SDAI_CONTRACT_ADDRESS)
48
+ .convertToAssets(CollateralToken(1).as_wei)
49
+ .as_token.value
50
+ )
51
+ in_wei = get_buy_token_amount(
52
+ sell_amount=CollateralToken(1).as_wei,
53
+ sell_token=token_address,
54
+ buy_token=WRAPPED_XDAI_CONTRACT_ADDRESS,
55
+ )
56
+ in_token = in_wei.as_token
57
+ return USD(in_token.value)
58
+
59
+
60
+ # A short cache to not spam CoW and prevent timeouts, but still have relatively fresh data.
61
+ @cached(TTLCache(maxsize=100, ttl=5 * 60))
62
+ def get_single_usd_to_token_rate(token_address: ChecksumAddress) -> CollateralToken:
63
+ # (w)xDai is a stable coin against USD, so use it to estimate USD worth.
64
+ if WRAPPED_XDAI_CONTRACT_ADDRESS == token_address:
65
+ return CollateralToken(1.0)
66
+ # sDai is ERC4626 with wxDai as asset, we can take the rate directly from there instead of calling CoW.
67
+ if SDAI_CONTRACT_ADDRESS == token_address:
68
+ return CollateralToken(
69
+ ContractERC4626OnGnosisChain(address=SDAI_CONTRACT_ADDRESS)
70
+ .convertToShares(CollateralToken(1).as_wei)
71
+ .as_token.value
72
+ )
73
+ in_wei = get_buy_token_amount(
74
+ sell_amount=CollateralToken(1).as_wei,
75
+ sell_token=WRAPPED_XDAI_CONTRACT_ADDRESS,
76
+ buy_token=token_address,
77
+ )
78
+ in_token = in_wei.as_token
79
+ return CollateralToken(in_token.value)
@@ -9,7 +9,13 @@ from pydantic import BaseModel, ValidationError
9
9
  from scipy.optimize import newton
10
10
  from scipy.stats import entropy
11
11
 
12
- from prediction_market_agent_tooling.gtypes import DatetimeUTC, Probability, SecretStr
12
+ from prediction_market_agent_tooling.gtypes import (
13
+ CollateralToken,
14
+ DatetimeUTC,
15
+ OutcomeToken,
16
+ Probability,
17
+ SecretStr,
18
+ )
13
19
  from prediction_market_agent_tooling.loggers import logger
14
20
  from prediction_market_agent_tooling.markets.market_fees import MarketFees
15
21
 
@@ -182,11 +188,11 @@ def prob_uncertainty(prob: Probability) -> float:
182
188
 
183
189
 
184
190
  def calculate_sell_amount_in_collateral(
185
- shares_to_sell: float,
186
- holdings: float,
187
- other_holdings: float,
191
+ shares_to_sell: OutcomeToken,
192
+ holdings: OutcomeToken,
193
+ other_holdings: OutcomeToken,
188
194
  fees: MarketFees,
189
- ) -> float:
195
+ ) -> CollateralToken:
190
196
  """
191
197
  Computes the amount of collateral that needs to be sold to get `shares`
192
198
  amount of shares. Returns None if the amount can't be computed.
@@ -199,11 +205,11 @@ def calculate_sell_amount_in_collateral(
199
205
  raise ValueError("All share args must be greater than 0")
200
206
 
201
207
  def f(r: float) -> float:
202
- R = (r + fees.absolute) / (1 - fees.bet_proportion)
208
+ R = OutcomeToken((r + fees.absolute) / (1 - fees.bet_proportion))
203
209
  first_term = other_holdings - R
204
210
  second_term = holdings + shares_to_sell - R
205
211
  third_term = holdings * other_holdings
206
- return (first_term * second_term) - third_term
212
+ return ((first_term * second_term) - third_term).value
207
213
 
208
214
  amount_to_sell = newton(f, 0)
209
- return float(amount_to_sell) * 0.999999 # Avoid rounding errors
215
+ return CollateralToken(float(amount_to_sell) * 0.999999) # Avoid rounding errors