prediction-market-agent-tooling 0.65.5__py3-none-any.whl → 0.69.17.dev1149__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 (88) hide show
  1. prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
  2. prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
  3. prediction_market_agent_tooling/abis/processor.abi.json +16 -0
  4. prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
  5. prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
  6. prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
  7. prediction_market_agent_tooling/benchmark/utils.py +13 -0
  8. prediction_market_agent_tooling/chains.py +1 -0
  9. prediction_market_agent_tooling/config.py +61 -2
  10. prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
  11. prediction_market_agent_tooling/deploy/agent.py +199 -67
  12. prediction_market_agent_tooling/deploy/agent_example.py +1 -1
  13. prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
  14. prediction_market_agent_tooling/deploy/constants.py +6 -0
  15. prediction_market_agent_tooling/gtypes.py +11 -1
  16. prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
  17. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
  18. prediction_market_agent_tooling/loggers.py +9 -1
  19. prediction_market_agent_tooling/logprobs_parser.py +2 -1
  20. prediction_market_agent_tooling/markets/agent_market.py +106 -18
  21. prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
  22. prediction_market_agent_tooling/markets/data_models.py +120 -7
  23. prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
  24. prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
  25. prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
  26. prediction_market_agent_tooling/markets/market_type.py +74 -0
  27. prediction_market_agent_tooling/markets/markets.py +7 -99
  28. prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
  29. prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
  30. prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
  31. prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
  32. prediction_market_agent_tooling/markets/omen/omen.py +112 -23
  33. prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
  34. prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
  35. prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
  36. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
  37. prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
  38. prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
  39. prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
  40. prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
  41. prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
  42. prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
  43. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
  44. prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
  45. prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
  46. prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
  47. prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
  48. prediction_market_agent_tooling/markets/seer/seer.py +393 -106
  49. prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
  50. prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
  51. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
  52. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
  53. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
  54. prediction_market_agent_tooling/tools/_generic_value.py +8 -2
  55. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
  56. prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
  57. prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
  58. prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
  59. prediction_market_agent_tooling/tools/contract.py +480 -38
  60. prediction_market_agent_tooling/tools/contract_utils.py +61 -0
  61. prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
  62. prediction_market_agent_tooling/tools/cow/models.py +122 -0
  63. prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
  64. prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
  65. prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
  66. prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
  67. prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
  68. prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
  69. prediction_market_agent_tooling/tools/openai_utils.py +31 -0
  70. prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
  71. prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
  72. prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
  73. prediction_market_agent_tooling/tools/rephrase.py +71 -0
  74. prediction_market_agent_tooling/tools/singleton.py +11 -6
  75. prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
  76. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
  77. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
  78. prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
  79. prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
  80. prediction_market_agent_tooling/tools/utils.py +61 -3
  81. prediction_market_agent_tooling/tools/web3_utils.py +63 -9
  82. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
  83. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
  84. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
  85. prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
  86. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
  87. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
  88. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info/licenses}/LICENSE +0 -0
@@ -1,29 +1,68 @@
1
1
  from pydantic import BaseModel, ConfigDict, Field
2
+ from web3 import Web3
2
3
  from web3.constants import ADDRESS_ZERO
3
4
 
4
5
  from prediction_market_agent_tooling.gtypes import (
6
+ ChecksumAddress,
5
7
  CollateralToken,
6
8
  HexAddress,
7
9
  HexBytes,
8
10
  OutcomeStr,
11
+ OutcomeToken,
12
+ Wei,
9
13
  )
14
+ from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
10
15
 
11
16
 
12
- class SeerToken(BaseModel):
17
+ class SwaprToken(BaseModel):
13
18
  id: HexBytes
14
19
  name: str
15
20
  symbol: str
16
21
 
22
+ @property
23
+ def address(self) -> ChecksumAddress:
24
+ return Web3.to_checksum_address(self.id.hex())
17
25
 
18
- class SeerPool(BaseModel):
26
+
27
+ class SwaprPool(BaseModel):
19
28
  model_config = ConfigDict(populate_by_name=True)
20
29
  id: HexBytes
21
30
  liquidity: int
22
- token0: SeerToken
23
- token1: SeerToken
31
+ token0: SwaprToken
32
+ token1: SwaprToken
24
33
  token0Price: CollateralToken
25
34
  token1Price: CollateralToken
26
35
  sqrtPrice: int
36
+ totalValueLockedToken0: float
37
+ totalValueLockedToken1: float
38
+
39
+
40
+ class SwaprSwap(BaseModel):
41
+ id: str # It's like "0x73afd8f096096552d72a0b40ea66d2076be136c6a531e2f6b190d151a750271e#32" (note the #32) # web3-private-key-ok
42
+ recipient: HexAddress
43
+ sender: HexAddress
44
+ price: Wei
45
+ amount0: CollateralToken
46
+ amount1: CollateralToken
47
+ token0: SwaprToken
48
+ token1: SwaprToken
49
+ timestamp: int
50
+
51
+ @property
52
+ def timestamp_utc(self) -> DatetimeUTC:
53
+ return DatetimeUTC.to_datetime_utc(self.timestamp)
54
+
55
+ @property
56
+ def added_to_pool(self) -> CollateralToken:
57
+ return self.amount0 if self.amount0 > 0 else self.amount1
58
+
59
+ @property
60
+ def withdrawn_from_pool(self) -> OutcomeToken:
61
+ return (
62
+ OutcomeToken(abs(self.amount0).value)
63
+ if self.amount0 < 0
64
+ else OutcomeToken(abs(self.amount1).value)
65
+ )
27
66
 
28
67
 
29
68
  class NewMarketEvent(BaseModel):
@@ -58,7 +97,3 @@ class CreateCategoricalMarketsParams(BaseModel):
58
97
  ) # typed as int for later .model_dump() usage (if using Wei, other keys also exported)
59
98
  opening_time: int = Field(..., alias="openingTime")
60
99
  token_names: list[str] = Field(..., alias="tokenNames")
61
-
62
-
63
- class SeerParentMarket(BaseModel):
64
- id: HexBytes
@@ -0,0 +1,80 @@
1
+ from web3 import Web3
2
+
3
+ from prediction_market_agent_tooling.config import APIKeys
4
+ from prediction_market_agent_tooling.gtypes import (
5
+ ChecksumAddress,
6
+ HexBytes,
7
+ HexStr,
8
+ TxReceipt,
9
+ Wei,
10
+ )
11
+ from prediction_market_agent_tooling.markets.seer.data_models import (
12
+ ExactInputSingleParams,
13
+ )
14
+ from prediction_market_agent_tooling.markets.seer.price_manager import PriceManager
15
+ from prediction_market_agent_tooling.markets.seer.seer_contracts import (
16
+ SwaprRouterContract,
17
+ )
18
+ from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
19
+ SeerSubgraphHandler,
20
+ )
21
+
22
+
23
+ class SwapPoolHandler:
24
+ def __init__(
25
+ self,
26
+ api_keys: APIKeys,
27
+ market_id: str,
28
+ collateral_token_address: ChecksumAddress,
29
+ seer_subgraph: SeerSubgraphHandler | None = None,
30
+ ):
31
+ self.api_keys = api_keys
32
+ self.market_id = market_id
33
+ self.collateral_token_address = collateral_token_address
34
+ self.seer_subgraph = seer_subgraph or SeerSubgraphHandler()
35
+
36
+ def _calculate_amount_out_minimum(
37
+ self,
38
+ amount_in: Wei,
39
+ token_in: ChecksumAddress,
40
+ token_out: ChecksumAddress,
41
+ buffer_pct: float = 0.05,
42
+ ) -> Wei:
43
+ price_manager = PriceManager.build(HexBytes(HexStr(self.market_id)))
44
+ value = price_manager.get_swapr_input_quote(
45
+ input_amount=amount_in, input_token=token_in, output_token=token_out
46
+ )
47
+ return value * (1 - buffer_pct)
48
+
49
+ def buy_or_sell_outcome_token(
50
+ self,
51
+ amount_in: Wei,
52
+ token_in: ChecksumAddress,
53
+ token_out: ChecksumAddress,
54
+ web3: Web3 | None = None,
55
+ ) -> TxReceipt:
56
+ """Buys/sells outcome_tokens in exchange for collateral tokens"""
57
+ if self.collateral_token_address not in [token_in, token_out]:
58
+ raise ValueError(
59
+ f"trading outcome_token for a token different than collateral_token {self.collateral_token_address} is not supported. {token_in=} {token_out=}"
60
+ )
61
+
62
+ amount_out_minimum = self._calculate_amount_out_minimum(
63
+ amount_in=amount_in,
64
+ token_in=token_in,
65
+ token_out=token_out,
66
+ )
67
+
68
+ p = ExactInputSingleParams(
69
+ token_in=token_in,
70
+ token_out=token_out,
71
+ recipient=self.api_keys.bet_from_address,
72
+ amount_in=amount_in,
73
+ amount_out_minimum=amount_out_minimum,
74
+ )
75
+
76
+ tx_receipt = SwaprRouterContract().exact_input_single(
77
+ api_keys=self.api_keys, params=p, web3=web3
78
+ )
79
+
80
+ return tx_receipt
@@ -60,6 +60,10 @@ class _GenericValue(
60
60
  cls.parser = parser
61
61
 
62
62
  def __init__(self, value: InputValueType) -> None:
63
+ if isinstance(value, str) and (
64
+ value.startswith("0x") or value.startswith("-0x")
65
+ ):
66
+ value = int(value, 16) # type: ignore[assignment] # mypy is confused but if `parser` below can process it, it should be fine, and if it cannot, it should be catched by mypy earlier than here.
63
67
  self.value: InternalValueType = self.parser(value)
64
68
  super().__init__({"value": self.value, "type": self.__class__.__name__})
65
69
 
@@ -187,9 +191,11 @@ class _GenericValue(
187
191
  raise TypeError("Cannot compare different types")
188
192
  return bool(self.value >= other.value)
189
193
 
190
- def __eq__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool: # type: ignore
194
+ def __eq__(self: GenericValueType, other: GenericValueType | t.Literal[0] | None) -> bool: # type: ignore
191
195
  if other == 0:
192
196
  other = self.zero()
197
+ if other is None:
198
+ return False
193
199
  if not isinstance(other, _GenericValue):
194
200
  raise TypeError("Cannot compare different types")
195
201
  if type(self) is not type(other):
@@ -239,7 +245,7 @@ class _GenericValue(
239
245
  cls, source_type: t.Any, handler: GetCoreSchemaHandler
240
246
  ) -> CoreSchema:
241
247
  # Support for Pydantic usage.
242
- dt_schema = handler(str | int | float | dict)
248
+ dt_schema = handler(cls.parser | str | dict)
243
249
  return core_schema.no_info_after_validator_function(
244
250
  lambda x: cls(x["value"] if isinstance(x, dict) else x),
245
251
  dt_schema,
@@ -1,6 +1,27 @@
1
- from prediction_market_agent_tooling.gtypes import CollateralToken, OutcomeToken
1
+ from enum import Enum
2
+ from itertools import chain
3
+ from typing import Callable
4
+
5
+ import numpy as np
6
+ from scipy.optimize import minimize
7
+
8
+ from prediction_market_agent_tooling.gtypes import (
9
+ CollateralToken,
10
+ OutcomeToken,
11
+ Probability,
12
+ )
13
+ from prediction_market_agent_tooling.loggers import logger
2
14
  from prediction_market_agent_tooling.markets.market_fees import MarketFees
3
- from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet
15
+ from prediction_market_agent_tooling.tools.betting_strategies.utils import (
16
+ BinaryKellyBet,
17
+ CategoricalKellyBet,
18
+ )
19
+ from prediction_market_agent_tooling.tools.utils import check_not_none
20
+
21
+
22
+ class KellyType(str, Enum):
23
+ SIMPLE = "simple"
24
+ FULL = "full"
4
25
 
5
26
 
6
27
  def check_is_valid_probability(probability: float) -> None:
@@ -13,7 +34,7 @@ def get_kelly_bet_simplified(
13
34
  market_p_yes: float,
14
35
  estimated_p_yes: float,
15
36
  confidence: float,
16
- ) -> SimpleBet:
37
+ ) -> BinaryKellyBet:
17
38
  """
18
39
  Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
19
40
 
@@ -39,22 +60,24 @@ def get_kelly_bet_simplified(
39
60
  if estimated_p_yes > market_p_yes:
40
61
  bet_direction = True
41
62
  market_prob = market_p_yes
63
+ estimated_p = estimated_p_yes
42
64
  else:
43
65
  bet_direction = False
44
66
  market_prob = 1 - market_p_yes
67
+ estimated_p = 1 - estimated_p_yes
45
68
 
46
69
  # Handle the case where market_prob is 0
47
70
  if market_prob == 0:
48
71
  market_prob = 1e-10
49
72
 
50
- edge = abs(estimated_p_yes - market_p_yes) * confidence
73
+ edge = abs(estimated_p - market_prob) * confidence
51
74
  odds = (1 / market_prob) - 1
52
75
  kelly_fraction = edge / odds
53
76
 
54
77
  # Ensure bet size is non-negative does not exceed the wallet balance
55
78
  bet_size = CollateralToken(min(kelly_fraction * max_bet.value, max_bet.value))
56
79
 
57
- return SimpleBet(direction=bet_direction, size=bet_size)
80
+ return BinaryKellyBet(direction=bet_direction, size=bet_size)
58
81
 
59
82
 
60
83
  def get_kelly_bet_full(
@@ -64,7 +87,7 @@ def get_kelly_bet_full(
64
87
  confidence: float,
65
88
  max_bet: CollateralToken,
66
89
  fees: MarketFees,
67
- ) -> SimpleBet:
90
+ ) -> BinaryKellyBet:
68
91
  """
69
92
  Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
70
93
 
@@ -98,7 +121,7 @@ def get_kelly_bet_full(
98
121
  check_is_valid_probability(confidence)
99
122
 
100
123
  if max_bet == 0:
101
- return SimpleBet(size=CollateralToken(0), direction=True)
124
+ return BinaryKellyBet(size=CollateralToken(0), direction=True)
102
125
 
103
126
  x = yes_outcome_pool_size.value
104
127
  y = no_outcome_pool_size.value
@@ -144,7 +167,247 @@ def get_kelly_bet_full(
144
167
  kelly_bet_amount = numerator / denominator
145
168
 
146
169
  # Clip the bet size to max_bet to account for rounding errors.
147
- return SimpleBet(
170
+ return BinaryKellyBet(
148
171
  direction=kelly_bet_amount > 0,
149
172
  size=CollateralToken(min(max_bet.value, abs(kelly_bet_amount))),
150
173
  )
174
+
175
+
176
+ def get_kelly_bets_categorical_simplified(
177
+ market_probabilities: list[Probability],
178
+ estimated_probabilities: list[Probability],
179
+ confidence: float,
180
+ max_bet: CollateralToken,
181
+ fees: MarketFees,
182
+ allow_multiple_bets: bool,
183
+ allow_shorting: bool,
184
+ bet_precision: int = 6,
185
+ ) -> list[CategoricalKellyBet]:
186
+ """
187
+ Calculate Kelly bets for categorical markets using only market probabilities.
188
+ Returns a list of CategoricalKellyBet objects, one for each outcome.
189
+ Considers max_bet across all outcomes together.
190
+ Indicates both buying (long) and shorting (selling) by allowing negative bet sizes.
191
+ """
192
+ for p in chain(market_probabilities, estimated_probabilities, [confidence]):
193
+ check_is_valid_probability(p)
194
+ assert len(market_probabilities) == len(
195
+ estimated_probabilities
196
+ ), "Mismatch in number of outcomes"
197
+
198
+ f = 1 - fees.bet_proportion
199
+
200
+ total_kelly_fraction = 0.0
201
+ kelly_fractions = []
202
+
203
+ for i in range(len(market_probabilities)):
204
+ estimated_p = estimated_probabilities[i]
205
+ market_p = max(market_probabilities[i], 1e-10)
206
+
207
+ edge = (estimated_p - market_p) * confidence
208
+ odds = (1 / market_p) - 1
209
+ kelly_fraction = edge / odds * f
210
+
211
+ if not allow_shorting:
212
+ kelly_fraction = max(0, kelly_fraction)
213
+
214
+ kelly_fractions.append(kelly_fraction)
215
+ total_kelly_fraction += abs(kelly_fraction)
216
+
217
+ best_kelly_fraction_index = max(
218
+ range(len(kelly_fractions)), key=lambda i: abs(kelly_fractions[i])
219
+ )
220
+
221
+ bets = []
222
+ for i, kelly_fraction in enumerate(kelly_fractions):
223
+ if not allow_multiple_bets:
224
+ bet_size = (
225
+ kelly_fraction * max_bet.value if i == best_kelly_fraction_index else 0
226
+ )
227
+ elif allow_multiple_bets and total_kelly_fraction > 0:
228
+ bet_size = (kelly_fraction / total_kelly_fraction) * max_bet.value
229
+ else:
230
+ bet_size = 0.0
231
+ # Ensure bet_size is within [-max_bet.value, max_bet.value]
232
+ bet_size = max(-max_bet.value, min(bet_size, max_bet.value))
233
+ bets.append(
234
+ CategoricalKellyBet(
235
+ index=i, size=CollateralToken(round(bet_size, bet_precision))
236
+ )
237
+ )
238
+
239
+ return bets
240
+
241
+
242
+ def get_kelly_bets_categorical_full(
243
+ market_probabilities: list[Probability],
244
+ estimated_probabilities: list[Probability],
245
+ confidence: float,
246
+ max_bet: CollateralToken,
247
+ fees: MarketFees,
248
+ allow_multiple_bets: bool,
249
+ allow_shorting: bool,
250
+ multicategorical: bool,
251
+ # investment amount, outcome index --> received outcome tokens
252
+ get_buy_token_amount: Callable[[CollateralToken, int], OutcomeToken],
253
+ bet_precision: int = 6,
254
+ ) -> list[CategoricalKellyBet]:
255
+ """
256
+ Calculate Kelly bets for categorical markets using joint optimization over all outcomes,
257
+ splitting the max bet between all possible outcomes to maximize expected log utility.
258
+ Returns a list of CategoricalKellyBet objects, one for each outcome.
259
+ Handles both buying (long) and shorting (selling) by allowing negative bet sizes.
260
+ If the agent's probabilities are very close to the market's, returns all-zero bets.
261
+ multicategorical means that multiple outcomes could be selected as correct ones.
262
+ """
263
+ assert len(market_probabilities) == len(
264
+ estimated_probabilities
265
+ ), "Mismatch in number of outcomes"
266
+ for p in chain(market_probabilities, estimated_probabilities, [confidence]):
267
+ check_is_valid_probability(p)
268
+
269
+ n = len(market_probabilities)
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 = get_buy_token_amount(CollateralToken(bets[i]), i)
287
+ payout += buy_result.value
288
+ else:
289
+ # If bet is negative, we "short" outcome i by buying all other outcomes
290
+ for j in range(n):
291
+ if j == i:
292
+ continue
293
+ buy_result = get_buy_token_amount(
294
+ CollateralToken(abs(bets[i]) / (n - 1)), j
295
+ )
296
+ payout += buy_result.value
297
+ payouts.append(payout)
298
+ return payouts
299
+
300
+ def adjust_prob(my_prob: float, market_prob: float) -> float:
301
+ # Based on the confidence, shrinks the predicted probability towards market's current probability.
302
+ return confidence * my_prob + (1 - confidence) * market_prob
303
+
304
+ # Use the simple version to estimate the initial bet vector.
305
+ x0 = np.array(
306
+ [
307
+ x.size.value # Use simplified value as starting point
308
+ for x in get_kelly_bets_categorical_simplified(
309
+ market_probabilities=market_probabilities,
310
+ estimated_probabilities=estimated_probabilities,
311
+ confidence=confidence,
312
+ max_bet=max_bet,
313
+ fees=fees,
314
+ allow_multiple_bets=allow_multiple_bets,
315
+ allow_shorting=allow_shorting,
316
+ bet_precision=bet_precision,
317
+ )
318
+ ]
319
+ )
320
+
321
+ # Track the best solution found during optimization
322
+ best_solution_bets = None
323
+ best_solution_utility = float("-inf")
324
+
325
+ def neg_expected_log_utility(bets: list[float]) -> float:
326
+ """
327
+ Negative expected log utility for categorical Kelly betting.
328
+ This function is minimized to find the optimal bet allocation.
329
+ """
330
+ adj_probs = [
331
+ adjust_prob(estimated_probabilities[i], market_probabilities[i])
332
+ for i in range(n)
333
+ ]
334
+ payouts = compute_payouts(bets)
335
+
336
+ profits = [payout - abs(bet) for payout, bet in zip(payouts, bets)]
337
+
338
+ # Ensure profits are not too negative to avoid log(negative) or log(0)
339
+ # Use a small epsilon to prevent numerical instability
340
+ min_profit = -0.99 # Ensure 1 + profit > 0.01
341
+ profits = [max(profit, min_profit) for profit in profits]
342
+
343
+ # Expected log utility
344
+ expected_log_utility: float = sum(
345
+ adj_probs[i] * np.log(1 + profits[i]) for i in range(n)
346
+ )
347
+
348
+ # Track the best solution found so far
349
+ nonlocal best_solution_bets, best_solution_utility
350
+ if expected_log_utility > best_solution_utility:
351
+ best_solution_bets = np.array(bets)
352
+ best_solution_utility = expected_log_utility
353
+
354
+ # Return negative for minimization
355
+ return -expected_log_utility
356
+
357
+ constraints = [
358
+ # We can not bet more than `max_bet_value`
359
+ {
360
+ "type": "ineq",
361
+ "fun": lambda bets: max_bet_value - np.sum(np.abs(bets)),
362
+ },
363
+ # Each bet should not result in guaranteed loss
364
+ {
365
+ "type": "ineq",
366
+ "fun": lambda bets: [
367
+ payout
368
+ - (sum(abs(b) for b in bets) if not multicategorical else abs(bets[i]))
369
+ for i, payout in enumerate(compute_payouts(bets))
370
+ ],
371
+ },
372
+ ]
373
+
374
+ result = minimize(
375
+ neg_expected_log_utility,
376
+ x0,
377
+ method="SLSQP",
378
+ bounds=[
379
+ ((-max_bet_value if allow_shorting else 0), max_bet_value) for _ in range(n)
380
+ ],
381
+ constraints=constraints,
382
+ options={"maxiter": 10_000},
383
+ )
384
+
385
+ # This can sometimes happen, as long as it's occasional, it's should be fine to just use simplified version approximation.
386
+ if not result.success:
387
+ logger.warning(
388
+ f"Joint optimization failed: {result=} {x0=} {estimated_probabilities=} {confidence=} {market_probabilities=}"
389
+ )
390
+
391
+ # Use the best solution found during optimization, not just the final result (result.x).
392
+ # This is important because SLSQP may end on a worse solution due to numerical issues.
393
+ bet_vector = check_not_none(best_solution_bets) if result.success else x0
394
+
395
+ if not allow_multiple_bets:
396
+ # If we are not allowing multiple bets, we need to ensure only one bet is non-zero.
397
+ # We can do this by taking the maximum bet and setting all others to zero.
398
+ # We do this, instead of enforcing it in with additional constraint,
399
+ # because such hard constraint is problematic for the solver and results in almost always failing to optimize.
400
+ max_bet_index = np.argmax(np.abs(bet_vector))
401
+ max_bet_value = bet_vector[max_bet_index]
402
+
403
+ bet_vector = np.zeros_like(bet_vector)
404
+ bet_vector[max_bet_index] = max_bet_value
405
+
406
+ bets = [
407
+ CategoricalKellyBet(
408
+ index=i, size=CollateralToken(round(bet_vector[i], bet_precision))
409
+ )
410
+ for i in range(n)
411
+ ]
412
+
413
+ 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