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,12 +1,11 @@
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
7
- from prediction_market_agent_tooling.deploy.constants import (
8
- INVALID_OUTCOME_LOWERCASE_IDENTIFIER,
9
- )
8
+ from prediction_market_agent_tooling.deploy.constants import is_invalid_outcome
10
9
  from prediction_market_agent_tooling.gtypes import (
11
10
  USD,
12
11
  CollateralToken,
@@ -15,7 +14,11 @@ from prediction_market_agent_tooling.gtypes import (
15
14
  Probability,
16
15
  )
17
16
  from prediction_market_agent_tooling.loggers import logger
18
- from prediction_market_agent_tooling.markets.agent_market import AgentMarket, MarketFees
17
+ from prediction_market_agent_tooling.markets.agent_market import (
18
+ AgentMarket,
19
+ MarketFees,
20
+ QuestionType,
21
+ )
19
22
  from prediction_market_agent_tooling.markets.data_models import (
20
23
  CategoricalProbabilisticAnswer,
21
24
  ExistingPosition,
@@ -23,14 +26,21 @@ from prediction_market_agent_tooling.markets.data_models import (
23
26
  Trade,
24
27
  TradeType,
25
28
  )
29
+ from prediction_market_agent_tooling.markets.market_type import MarketType
26
30
  from prediction_market_agent_tooling.markets.omen.omen import (
27
31
  get_buy_outcome_token_amount,
28
32
  )
29
33
  from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
34
+ KellyType,
30
35
  get_kelly_bet_full,
31
36
  get_kelly_bet_simplified,
37
+ get_kelly_bets_categorical_full,
38
+ get_kelly_bets_categorical_simplified,
39
+ )
40
+ from prediction_market_agent_tooling.tools.betting_strategies.utils import (
41
+ BinaryKellyBet,
42
+ CategoricalKellyBet,
32
43
  )
33
- from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet
34
44
  from prediction_market_agent_tooling.tools.utils import check_not_none
35
45
 
36
46
 
@@ -39,6 +49,21 @@ class GuaranteedLossError(RuntimeError):
39
49
 
40
50
 
41
51
  class BettingStrategy(ABC):
52
+ supported_question_types: set[QuestionType]
53
+ supported_market_types: set[MarketType]
54
+
55
+ def __init__(self, take_profit: bool = True) -> None:
56
+ self.take_profit = take_profit
57
+
58
+ def is_market_supported(self, market: AgentMarket) -> bool:
59
+ if market.question_type not in self.supported_question_types:
60
+ return False
61
+
62
+ if MarketType.from_market(market) not in self.supported_market_types:
63
+ return False
64
+
65
+ return True
66
+
42
67
  @abstractmethod
43
68
  def calculate_trades(
44
69
  self,
@@ -80,7 +105,7 @@ class BettingStrategy(ABC):
80
105
 
81
106
  if outcome_tokens_to_get_in_usd <= trade.amount:
82
107
  raise GuaranteedLossError(
83
- f"Trade {trade=} would result in guaranteed loss by getting only {outcome_tokens_to_get=}. Halting execution."
108
+ f"Trade {trade=} on market {market.url=} would result in guaranteed loss by getting only {outcome_tokens_to_get=}. Halting execution."
84
109
  )
85
110
 
86
111
  clean_trades.append(trade)
@@ -94,6 +119,76 @@ class BettingStrategy(ABC):
94
119
  )
95
120
  return trades
96
121
 
122
+ @staticmethod
123
+ def cap_to_profitable_bet_amount(
124
+ market: AgentMarket,
125
+ bet_amount: USD,
126
+ outcome: OutcomeStr,
127
+ iters: int = 10,
128
+ ) -> USD:
129
+ """
130
+ Use a binary search (tree-based search) to efficiently find the largest profitable bet amount.
131
+ """
132
+ # First, try it with the desired amount right away.
133
+ if (
134
+ market.get_in_usd(
135
+ check_not_none(
136
+ market.get_buy_token_amount(bet_amount, outcome)
137
+ ).as_token
138
+ )
139
+ > bet_amount
140
+ ):
141
+ return bet_amount
142
+
143
+ # If it wasn't profitable, try binary search to find the highest, but profitable, amount.
144
+ lower = USD(0)
145
+ # It doesn't make sense to try to bet more than the liquidity itself, so override it as maximal value if it's lower.
146
+ upper = min(bet_amount, market.get_in_usd(market.get_liquidity()))
147
+ best_profitable = USD(0)
148
+
149
+ for _ in range(iters):
150
+ mid = (lower + upper) / 2
151
+ potential_outcome_value = market.get_in_usd(
152
+ check_not_none(market.get_buy_token_amount(mid, outcome)).as_token
153
+ )
154
+
155
+ if potential_outcome_value > mid:
156
+ # Profitable, try higher
157
+ best_profitable = mid
158
+ lower = mid
159
+
160
+ else:
161
+ # Not profitable, try lower
162
+ upper = mid
163
+
164
+ # If the search interval is very small, break early
165
+ if float(upper - lower) < 1e-8:
166
+ break
167
+
168
+ if np.isclose(best_profitable.value, 0):
169
+ best_profitable = USD(0)
170
+
171
+ return best_profitable
172
+
173
+ @staticmethod
174
+ def cap_to_profitable_position(
175
+ market: AgentMarket,
176
+ existing_position: USD,
177
+ wanted_position: USD,
178
+ outcome_to_bet_on: OutcomeStr,
179
+ ) -> USD:
180
+ # If the wanted position is lower, it means the agent is gonna sell and that's profitable always.
181
+ if wanted_position > existing_position:
182
+ difference = wanted_position - existing_position
183
+ # Cap the difference we would like to buy to a profitable one.
184
+ capped_difference = BettingStrategy.cap_to_profitable_bet_amount(
185
+ market, difference, outcome_to_bet_on
186
+ )
187
+ # Lowered the actual wanted position such that it remains profitable.
188
+ wanted_position = existing_position + capped_difference
189
+
190
+ return wanted_position
191
+
97
192
  def _build_rebalance_trades_from_positions(
98
193
  self,
99
194
  existing_position: ExistingPosition | None,
@@ -138,10 +233,22 @@ class BettingStrategy(ABC):
138
233
  )
139
234
 
140
235
  diff_amount = target_amount - existing_amount
236
+
141
237
  if diff_amount == 0:
142
238
  continue
143
239
 
144
240
  trade_type = TradeType.SELL if diff_amount < 0 else TradeType.BUY
241
+
242
+ # We work with positions, so imagine following scenario: Agent invested $10 when probs were 50:50,
243
+ # now the probs are 99:1 and his initial $10 is worth $100.
244
+ # If `take_profit` is set to False, agent won't sell the $90 to get back to the $10 position.
245
+ if (
246
+ not self.take_profit
247
+ and target_amount > 0
248
+ and trade_type == TradeType.SELL
249
+ ):
250
+ continue
251
+
145
252
  trade = Trade(
146
253
  amount=abs(diff_amount),
147
254
  outcome=outcome,
@@ -159,13 +266,21 @@ class BettingStrategy(ABC):
159
266
  return trades
160
267
 
161
268
 
162
- class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
163
- def __init__(self, bet_amount: USD):
164
- self.bet_amount = bet_amount
269
+ class CategoricalMaxAccuracyBettingStrategy(BettingStrategy):
270
+ supported_question_types = {
271
+ QuestionType.BINARY,
272
+ QuestionType.CATEGORICAL,
273
+ QuestionType.SCALAR,
274
+ }
275
+ supported_market_types = {x for x in MarketType if x.is_trading_market}
276
+
277
+ def __init__(self, max_position_amount: USD, take_profit: bool = True):
278
+ super().__init__(take_profit=take_profit)
279
+ self.max_position_amount = max_position_amount
165
280
 
166
281
  @property
167
282
  def maximum_possible_bet_amount(self) -> USD:
168
- return self.bet_amount
283
+ return self.max_position_amount
169
284
 
170
285
  @staticmethod
171
286
  def calculate_direction(
@@ -185,7 +300,7 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
185
300
  ) -> OutcomeStr:
186
301
  # We get the first direction which is != direction.
187
302
  other_direction = [i for i in outcomes if i.lower() != direction.lower()][0]
188
- if INVALID_OUTCOME_LOWERCASE_IDENTIFIER in other_direction.lower():
303
+ if is_invalid_outcome(other_direction):
189
304
  raise ValueError("Invalid outcome found as opposite direction. Exitting.")
190
305
  return other_direction
191
306
 
@@ -196,11 +311,23 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
196
311
  market: AgentMarket,
197
312
  ) -> list[Trade]:
198
313
  """We place bet on only one outcome."""
199
-
200
314
  outcome_to_bet_on = self.calculate_direction(market, answer)
201
315
 
316
+ # Will be lowered if the amount that we would need to buy would be unprofitable.
317
+ actual_wanted_position = BettingStrategy.cap_to_profitable_position(
318
+ market,
319
+ (
320
+ existing_position.amounts_current.get(outcome_to_bet_on, USD(0))
321
+ if existing_position
322
+ else USD(0)
323
+ ),
324
+ self.max_position_amount,
325
+ outcome_to_bet_on,
326
+ )
327
+
202
328
  target_position = Position(
203
- market_id=market.id, amounts_current={outcome_to_bet_on: self.bet_amount}
329
+ market_id=market.id,
330
+ amounts_current={outcome_to_bet_on: actual_wanted_position},
204
331
  )
205
332
  trades = self._build_rebalance_trades_from_positions(
206
333
  existing_position=existing_position,
@@ -209,8 +336,11 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
209
336
  )
210
337
  return trades
211
338
 
339
+ def __repr__(self) -> str:
340
+ return f"CategoricalMaxAccuracyBettingStrategy(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
341
+
212
342
 
213
- class MaxExpectedValueBettingStrategy(MultiCategoricalMaxAccuracyBettingStrategy):
343
+ class MaxExpectedValueBettingStrategy(CategoricalMaxAccuracyBettingStrategy):
214
344
  @staticmethod
215
345
  def calculate_direction(
216
346
  market: AgentMarket, answer: CategoricalProbabilisticAnswer
@@ -251,43 +381,54 @@ class MaxExpectedValueBettingStrategy(MultiCategoricalMaxAccuracyBettingStrategy
251
381
 
252
382
  return best_outcome
253
383
 
384
+ def __repr__(self) -> str:
385
+ return f"MaxExpectedValueBettingStrategy(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
386
+
254
387
 
255
- 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
388
+ class _BinaryKellyBettingStrategy(BettingStrategy):
389
+ supported_question_types = {QuestionType.BINARY}
390
+
391
+ def __init__(
392
+ self,
393
+ kelly_type: KellyType,
394
+ max_position_amount: USD,
395
+ max_price_impact: float | None = None,
396
+ take_profit: bool = True,
397
+ ):
398
+ super().__init__(take_profit=take_profit)
399
+ self.kelly_type = kelly_type
400
+ self.max_position_amount = max_position_amount
258
401
  self.max_price_impact = max_price_impact
259
402
 
260
403
  @property
261
404
  def maximum_possible_bet_amount(self) -> USD:
262
- return self.max_bet_amount
405
+ return self.max_position_amount
263
406
 
264
- @staticmethod
265
407
  def get_kelly_bet(
408
+ self,
266
409
  market: AgentMarket,
267
- max_bet_amount: USD,
268
410
  direction: OutcomeStr,
269
411
  other_direction: OutcomeStr,
270
412
  answer: CategoricalProbabilisticAnswer,
271
413
  override_p_yes: float | None = None,
272
- ) -> SimpleBet:
414
+ ) -> BinaryKellyBet:
415
+ if not market.is_binary:
416
+ raise ValueError("This strategy is usable only with binary markets.")
417
+
273
418
  estimated_p_yes = (
274
419
  answer.probability_for_market_outcome(direction)
275
420
  if not override_p_yes
276
421
  else override_p_yes
277
422
  )
278
423
 
279
- if not market.is_binary:
280
- # use Kelly simple, since Kelly full only supports 2 outcomes
281
-
424
+ if self.kelly_type == KellyType.SIMPLE:
282
425
  kelly_bet = get_kelly_bet_simplified(
283
- max_bet=market.get_usd_in_token(max_bet_amount),
426
+ max_bet=market.get_usd_in_token(self.max_position_amount),
284
427
  market_p_yes=market.probability_for_market_outcome(direction),
285
428
  estimated_p_yes=estimated_p_yes,
286
429
  confidence=answer.confidence,
287
430
  )
288
431
  else:
289
- # We consider only binary markets, since the Kelly strategy is not yet implemented
290
- # for markets with more than 2 outcomes (https://github.com/gnosis/prediction-market-agent-tooling/issues/671).
291
432
  direction_to_bet_pool_size = market.get_outcome_token_pool_by_outcome(
292
433
  direction
293
434
  )
@@ -298,7 +439,7 @@ class KellyBettingStrategy(BettingStrategy):
298
439
  yes_outcome_pool_size=direction_to_bet_pool_size,
299
440
  no_outcome_pool_size=other_direction_pool_size,
300
441
  estimated_p_yes=estimated_p_yes,
301
- max_bet=market.get_usd_in_token(max_bet_amount),
442
+ max_bet=market.get_usd_in_token(self.max_position_amount),
302
443
  confidence=answer.confidence,
303
444
  fees=market.fees,
304
445
  )
@@ -311,17 +452,16 @@ class KellyBettingStrategy(BettingStrategy):
311
452
  market: AgentMarket,
312
453
  ) -> list[Trade]:
313
454
  # We consider the p_yes as the direction with highest probability.
314
- direction = MultiCategoricalMaxAccuracyBettingStrategy.calculate_direction(
455
+ direction = CategoricalMaxAccuracyBettingStrategy.calculate_direction(
315
456
  market, answer
316
457
  )
317
458
  # We get the first direction which is != direction.
318
459
  other_direction = [i for i in market.outcomes if i != direction][0]
319
- if INVALID_OUTCOME_LOWERCASE_IDENTIFIER in other_direction.lower():
460
+ if is_invalid_outcome(other_direction):
320
461
  raise ValueError("Invalid outcome found as opposite direction. Exitting.")
321
462
 
322
463
  kelly_bet = self.get_kelly_bet(
323
464
  market=market,
324
- max_bet_amount=self.max_bet_amount,
325
465
  direction=direction,
326
466
  other_direction=other_direction,
327
467
  answer=answer,
@@ -331,15 +471,24 @@ class KellyBettingStrategy(BettingStrategy):
331
471
  if self.max_price_impact:
332
472
  # Adjust amount
333
473
  max_price_impact_bet_amount = self.calculate_bet_amount_for_price_impact(
334
- market, kelly_bet, direction=direction
474
+ market,
475
+ direction=direction,
476
+ max_price_impact=self.max_price_impact,
335
477
  )
336
478
 
337
479
  # We just don't want Kelly size to extrapolate price_impact - hence we take the min.
338
480
  kelly_bet_size = min(kelly_bet.size, max_price_impact_bet_amount)
339
481
 
340
482
  bet_outcome = direction if kelly_bet.direction else other_direction
483
+
341
484
  amounts = {
342
- bet_outcome: market.get_token_in_usd(kelly_bet_size),
485
+ bet_outcome: (
486
+ BettingStrategy.cap_to_profitable_bet_amount(
487
+ market, market.get_token_in_usd(kelly_bet_size), bet_outcome
488
+ )
489
+ if kelly_bet_size > 0
490
+ else USD(0)
491
+ ),
343
492
  }
344
493
  target_position = Position(market_id=market.id, amounts_current=amounts)
345
494
  trades = self._build_rebalance_trades_from_positions(
@@ -347,8 +496,8 @@ class KellyBettingStrategy(BettingStrategy):
347
496
  )
348
497
  return trades
349
498
 
499
+ @staticmethod
350
500
  def calculate_price_impact_for_bet_amount(
351
- self,
352
501
  outcome_idx: int,
353
502
  bet_amount: CollateralToken,
354
503
  pool_balances: list[OutcomeWei],
@@ -366,29 +515,31 @@ class KellyBettingStrategy(BettingStrategy):
366
515
  price_impact = (actual_price - expected_price) / expected_price
367
516
  return price_impact
368
517
 
518
+ @staticmethod
369
519
  def calculate_bet_amount_for_price_impact(
370
- self, market: AgentMarket, kelly_bet: SimpleBet, direction: OutcomeStr
520
+ market: AgentMarket,
521
+ direction: OutcomeStr,
522
+ max_price_impact: float,
371
523
  ) -> CollateralToken:
372
524
  def calculate_price_impact_deviation_from_target_price_impact(
373
- bet_amount_usd: float, # Needs to be float because it's used in minimize_scalar internally.
525
+ bet_amount_collateral: float, # Needs to be float because it's used in minimize_scalar internally.
374
526
  ) -> float:
375
527
  outcome_idx = market.get_outcome_index(direction)
376
- price_impact = self.calculate_price_impact_for_bet_amount(
377
- outcome_idx=outcome_idx,
378
- bet_amount=market.get_usd_in_token(USD(bet_amount_usd)),
379
- pool_balances=pool_balances,
380
- fees=market.fees,
528
+ price_impact = (
529
+ _BinaryKellyBettingStrategy.calculate_price_impact_for_bet_amount(
530
+ outcome_idx=outcome_idx,
531
+ bet_amount=CollateralToken(bet_amount_collateral),
532
+ pool_balances=pool_balances,
533
+ fees=market.fees,
534
+ )
381
535
  )
382
536
  # We return abs for the algorithm to converge to 0 instead of the min (and possibly negative) value.
383
-
384
- max_price_impact = check_not_none(self.max_price_impact)
385
537
  return abs(price_impact - max_price_impact)
386
538
 
387
539
  if not market.outcome_token_pool:
388
- logger.warning(
540
+ raise ValueError(
389
541
  "Market outcome_token_pool is None, cannot calculate bet amount"
390
542
  )
391
- return kelly_bet.size
392
543
 
393
544
  pool_balances = [i.as_outcome_wei for i in market.outcome_token_pool.values()]
394
545
  # stay float for compatibility with `minimize_scalar`
@@ -399,30 +550,64 @@ class KellyBettingStrategy(BettingStrategy):
399
550
  calculate_price_impact_deviation_from_target_price_impact,
400
551
  bounds=(0, 1000 * total_pool_balance),
401
552
  method="bounded",
402
- tol=1e-11,
553
+ tol=1e-13,
403
554
  options={"maxiter": 10000},
404
555
  )
405
556
  return CollateralToken(optimized_bet_amount.x)
406
557
 
407
558
  def __repr__(self) -> str:
408
- return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount}, max_price_impact={self.max_price_impact})"
559
+ return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, max_price_impact={self.max_price_impact}, take_profit={self.take_profit})"
560
+
561
+
562
+ class SimpleBinaryKellyBettingStrategy(_BinaryKellyBettingStrategy):
563
+ supported_market_types = {x for x in MarketType if x.is_trading_market}
564
+
565
+ def __init__(
566
+ self,
567
+ max_position_amount: USD,
568
+ take_profit: bool = True,
569
+ ):
570
+ super().__init__(
571
+ kelly_type=KellyType.SIMPLE,
572
+ max_position_amount=max_position_amount,
573
+ max_price_impact=None,
574
+ take_profit=take_profit,
575
+ )
576
+
577
+
578
+ class FullBinaryKellyBettingStrategy(_BinaryKellyBettingStrategy):
579
+ # Supports only OMEN because it uses closed-form formula derived from a binary FPMM.
580
+ supported_market_types = {MarketType.OMEN}
581
+
582
+ def __init__(
583
+ self,
584
+ max_position_amount: USD,
585
+ max_price_impact: float | None = None,
586
+ take_profit: bool = True,
587
+ ):
588
+ super().__init__(
589
+ kelly_type=KellyType.FULL,
590
+ max_position_amount=max_position_amount,
591
+ max_price_impact=max_price_impact,
592
+ take_profit=take_profit,
593
+ )
409
594
 
410
595
 
411
596
  class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
412
- def __init__(self, max_bet_amount: USD):
413
- self.max_bet_amount = max_bet_amount
597
+ supported_question_types = {QuestionType.BINARY}
598
+ supported_market_types = {MarketType.OMEN}
599
+
600
+ def __init__(
601
+ self,
602
+ max_position_amount: USD,
603
+ take_profit: bool = True,
604
+ ):
605
+ super().__init__(take_profit)
606
+ self.max_position_amount = max_position_amount
414
607
 
415
608
  @property
416
609
  def maximum_possible_bet_amount(self) -> USD:
417
- return self.max_bet_amount
418
-
419
- def adjust_bet_amount(
420
- self, existing_position: ExistingPosition | None, market: AgentMarket
421
- ) -> USD:
422
- existing_position_total_amount = (
423
- existing_position.total_amount_current if existing_position else USD(0)
424
- )
425
- return self.max_bet_amount + existing_position_total_amount
610
+ return self.max_position_amount
426
611
 
427
612
  def calculate_trades(
428
613
  self,
@@ -430,26 +615,23 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
430
615
  answer: CategoricalProbabilisticAnswer,
431
616
  market: AgentMarket,
432
617
  ) -> list[Trade]:
433
- adjusted_bet_amount_usd = self.adjust_bet_amount(existing_position, market)
434
-
435
618
  outcome = get_most_probable_outcome(answer.probabilities)
436
619
 
437
- direction = MultiCategoricalMaxAccuracyBettingStrategy.calculate_direction(
620
+ direction = CategoricalMaxAccuracyBettingStrategy.calculate_direction(
438
621
  market, answer
439
622
  )
440
623
  # We get the first direction which is != direction.
441
- other_direction = (
442
- MultiCategoricalMaxAccuracyBettingStrategy.get_other_direction(
443
- outcomes=market.outcomes, direction=direction
444
- )
624
+ other_direction = CategoricalMaxAccuracyBettingStrategy.get_other_direction(
625
+ outcomes=market.outcomes, direction=direction
445
626
  )
446
627
 
447
628
  # We ignore the direction nudge given by Kelly, hence we assume we have a perfect prediction.
448
629
  estimated_p_yes = 1.0
449
630
 
450
- kelly_bet = KellyBettingStrategy.get_kelly_bet(
631
+ kelly_bet = FullBinaryKellyBettingStrategy(
632
+ max_position_amount=self.max_position_amount
633
+ ).get_kelly_bet(
451
634
  market=market,
452
- max_bet_amount=adjusted_bet_amount_usd,
453
635
  direction=direction,
454
636
  other_direction=other_direction,
455
637
  answer=answer,
@@ -471,4 +653,166 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
471
653
  return trades
472
654
 
473
655
  def __repr__(self) -> str:
474
- return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount})"
656
+ return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
657
+
658
+
659
+ class _CategoricalKellyBettingStrategy(BettingStrategy):
660
+ supported_question_types = {QuestionType.BINARY, QuestionType.CATEGORICAL}
661
+ supported_market_types = {x for x in MarketType if x.is_trading_market}
662
+
663
+ def __init__(
664
+ self,
665
+ kelly_type: KellyType,
666
+ max_position_amount: USD,
667
+ max_price_impact: float | None,
668
+ allow_multiple_bets: bool,
669
+ allow_shorting: bool,
670
+ multicategorical: bool,
671
+ take_profit: bool = True,
672
+ ):
673
+ super().__init__(take_profit=take_profit)
674
+ self.kelly_type = kelly_type
675
+ self.max_position_amount = max_position_amount
676
+ self.max_price_impact = max_price_impact
677
+ self.allow_multiple_bets = allow_multiple_bets
678
+ self.allow_shorting = allow_shorting
679
+ self.multicategorical = multicategorical
680
+
681
+ @property
682
+ def maximum_possible_bet_amount(self) -> USD:
683
+ return self.max_position_amount
684
+
685
+ def get_kelly_bets(
686
+ self,
687
+ market: AgentMarket,
688
+ max_bet_amount: USD,
689
+ answer: CategoricalProbabilisticAnswer,
690
+ ) -> list[CategoricalKellyBet]:
691
+ max_bet = market.get_usd_in_token(max_bet_amount)
692
+
693
+ if self.kelly_type == KellyType.SIMPLE:
694
+ kelly_bets = get_kelly_bets_categorical_simplified(
695
+ market_probabilities=[market.probabilities[o] for o in market.outcomes],
696
+ estimated_probabilities=[
697
+ answer.probability_for_market_outcome(o) for o in market.outcomes
698
+ ],
699
+ confidence=answer.confidence,
700
+ max_bet=max_bet,
701
+ fees=market.fees,
702
+ allow_multiple_bets=self.allow_multiple_bets,
703
+ allow_shorting=self.allow_shorting,
704
+ )
705
+
706
+ else:
707
+ kelly_bets = get_kelly_bets_categorical_full(
708
+ market_probabilities=[
709
+ market.probability_for_market_outcome(o) for o in market.outcomes
710
+ ],
711
+ estimated_probabilities=[
712
+ answer.probability_for_market_outcome(o) for o in market.outcomes
713
+ ],
714
+ confidence=answer.confidence,
715
+ max_bet=max_bet,
716
+ fees=market.fees,
717
+ allow_multiple_bets=self.allow_multiple_bets,
718
+ allow_shorting=self.allow_shorting,
719
+ multicategorical=self.multicategorical,
720
+ get_buy_token_amount=lambda bet_amount, outcome_index: check_not_none(
721
+ market.get_buy_token_amount(
722
+ bet_amount, market.get_outcome_str(outcome_index)
723
+ )
724
+ ),
725
+ )
726
+
727
+ return kelly_bets
728
+
729
+ def calculate_trades(
730
+ self,
731
+ existing_position: ExistingPosition | None,
732
+ answer: CategoricalProbabilisticAnswer,
733
+ market: AgentMarket,
734
+ ) -> list[Trade]:
735
+ kelly_bets = self.get_kelly_bets(
736
+ market=market,
737
+ max_bet_amount=self.max_position_amount,
738
+ answer=answer,
739
+ )
740
+
741
+ # TODO: Allow shorting in BettingStrategy._build_rebalance_trades_from_positions.
742
+ # In binary implementation, we simply flip the direction in case of negative bet, for categorical outcome, we need to implement shorting.
743
+ kelly_bets = [bet for bet in kelly_bets if bet.size > 0]
744
+ if not kelly_bets:
745
+ return []
746
+
747
+ # TODO: Allow betting on multiple outcomes.
748
+ # Categorical kelly could suggest to bet on multiple outcomes, but we only consider the first one for now (limitation of BettingStrategy `trades` creation).
749
+ # Also, this could maybe work for multi-categorical markets as well, but it wasn't benchmarked for it.
750
+ best_kelly_bet = max(kelly_bets, key=lambda x: abs(x.size))
751
+
752
+ if self.max_price_impact:
753
+ # Adjust amount
754
+ max_price_impact_bet_amount = (
755
+ _BinaryKellyBettingStrategy.calculate_bet_amount_for_price_impact(
756
+ market,
757
+ direction=market.get_outcome_str(best_kelly_bet.index),
758
+ max_price_impact=self.max_price_impact,
759
+ )
760
+ )
761
+ # We just don't want Kelly size to extrapolate price_impact - hence we take the min.
762
+ best_kelly_bet.size = min(best_kelly_bet.size, max_price_impact_bet_amount)
763
+
764
+ amounts = {
765
+ market.outcomes[best_kelly_bet.index]: market.get_token_in_usd(
766
+ best_kelly_bet.size
767
+ ),
768
+ }
769
+ target_position = Position(market_id=market.id, amounts_current=amounts)
770
+ trades = self._build_rebalance_trades_from_positions(
771
+ existing_position, target_position, market=market
772
+ )
773
+
774
+ return trades
775
+
776
+ def __repr__(self) -> str:
777
+ return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, max_price_impact={self.max_price_impact}, allow_multiple_bets={self.allow_multiple_bets}, allow_shorting={self.allow_shorting}, take_profit={self.take_profit})"
778
+
779
+
780
+ class SimpleCategoricalKellyBettingStrategy(_CategoricalKellyBettingStrategy):
781
+ def __init__(
782
+ self,
783
+ max_position_amount: USD,
784
+ allow_multiple_bets: bool,
785
+ allow_shorting: bool,
786
+ multicategorical: bool,
787
+ take_profit: bool = True,
788
+ ):
789
+ super().__init__(
790
+ kelly_type=KellyType.SIMPLE,
791
+ max_position_amount=max_position_amount,
792
+ max_price_impact=None,
793
+ allow_multiple_bets=allow_multiple_bets,
794
+ allow_shorting=allow_shorting,
795
+ multicategorical=multicategorical,
796
+ take_profit=take_profit,
797
+ )
798
+
799
+
800
+ class FullCategoricalKellyBettingStrategy(_CategoricalKellyBettingStrategy):
801
+ def __init__(
802
+ self,
803
+ max_position_amount: USD,
804
+ max_price_impact: float | None,
805
+ allow_multiple_bets: bool,
806
+ allow_shorting: bool,
807
+ multicategorical: bool,
808
+ take_profit: bool = True,
809
+ ):
810
+ super().__init__(
811
+ kelly_type=KellyType.FULL,
812
+ max_position_amount=max_position_amount,
813
+ max_price_impact=max_price_impact,
814
+ allow_multiple_bets=allow_multiple_bets,
815
+ allow_shorting=allow_shorting,
816
+ multicategorical=multicategorical,
817
+ take_profit=take_profit,
818
+ )