prediction-market-agent-tooling 0.67.3__py3-none-any.whl → 0.67.4.dev992__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prediction_market_agent_tooling/deploy/agent.py +2 -4
- prediction_market_agent_tooling/deploy/betting_strategy.py +169 -49
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +2 -2
- prediction_market_agent_tooling/markets/agent_market.py +17 -0
- prediction_market_agent_tooling/markets/blockchain_utils.py +5 -3
- prediction_market_agent_tooling/markets/data_models.py +23 -3
- prediction_market_agent_tooling/markets/omen/data_models.py +6 -1
- prediction_market_agent_tooling/markets/omen/omen.py +43 -6
- prediction_market_agent_tooling/markets/polymarket/api.py +9 -3
- prediction_market_agent_tooling/markets/polymarket/data_models.py +5 -3
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +15 -7
- prediction_market_agent_tooling/markets/seer/seer.py +11 -2
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +4 -1
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +276 -8
- prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
- prediction_market_agent_tooling/tools/hexbytes_custom.py +9 -0
- prediction_market_agent_tooling/tools/httpx_cached_client.py +5 -3
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +4 -3
- prediction_market_agent_tooling/tools/singleton.py +11 -6
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/METADATA +1 -1
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/RECORD +24 -24
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/LICENSE +0 -0
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/WHEEL +0 -0
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/entry_points.txt +0 -0
@@ -11,7 +11,7 @@ from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
11
11
|
from prediction_market_agent_tooling.config import APIKeys
|
12
12
|
from prediction_market_agent_tooling.deploy.betting_strategy import (
|
13
13
|
BettingStrategy,
|
14
|
-
|
14
|
+
CategoricalMaxAccuracyBettingStrategy,
|
15
15
|
TradeType,
|
16
16
|
)
|
17
17
|
from prediction_market_agent_tooling.deploy.trade_interval import (
|
@@ -662,9 +662,7 @@ class DeployableTraderAgent(DeployablePredictionAgent):
|
|
662
662
|
Given the market and prediction, agent uses this method to calculate optimal outcome and bet size.
|
663
663
|
"""
|
664
664
|
total_amount = self.get_total_amount_to_bet(market)
|
665
|
-
return
|
666
|
-
max_position_amount=total_amount
|
667
|
-
)
|
665
|
+
return CategoricalMaxAccuracyBettingStrategy(max_position_amount=total_amount)
|
668
666
|
|
669
667
|
def build_trades(
|
670
668
|
self,
|
@@ -30,8 +30,13 @@ from prediction_market_agent_tooling.markets.omen.omen import (
|
|
30
30
|
from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
|
31
31
|
get_kelly_bet_full,
|
32
32
|
get_kelly_bet_simplified,
|
33
|
+
get_kelly_bets_categorical_full,
|
34
|
+
get_kelly_bets_categorical_simplified,
|
35
|
+
)
|
36
|
+
from prediction_market_agent_tooling.tools.betting_strategies.utils import (
|
37
|
+
BinaryKellyBet,
|
38
|
+
CategoricalKellyBet,
|
33
39
|
)
|
34
|
-
from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet
|
35
40
|
from prediction_market_agent_tooling.tools.utils import check_not_none
|
36
41
|
|
37
42
|
|
@@ -245,7 +250,7 @@ class BettingStrategy(ABC):
|
|
245
250
|
return trades
|
246
251
|
|
247
252
|
|
248
|
-
class
|
253
|
+
class CategoricalMaxAccuracyBettingStrategy(BettingStrategy):
|
249
254
|
def __init__(self, max_position_amount: USD, take_profit: bool = True):
|
250
255
|
super().__init__(take_profit=take_profit)
|
251
256
|
self.max_position_amount = max_position_amount
|
@@ -308,8 +313,11 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
|
|
308
313
|
)
|
309
314
|
return trades
|
310
315
|
|
316
|
+
def __repr__(self) -> str:
|
317
|
+
return f"CategoricalMaxAccuracyBettingStrategy(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
|
318
|
+
|
311
319
|
|
312
|
-
class MaxExpectedValueBettingStrategy(
|
320
|
+
class MaxExpectedValueBettingStrategy(CategoricalMaxAccuracyBettingStrategy):
|
313
321
|
@staticmethod
|
314
322
|
def calculate_direction(
|
315
323
|
market: AgentMarket, answer: CategoricalProbabilisticAnswer
|
@@ -350,49 +358,49 @@ class MaxExpectedValueBettingStrategy(MultiCategoricalMaxAccuracyBettingStrategy
|
|
350
358
|
|
351
359
|
return best_outcome
|
352
360
|
|
361
|
+
def __repr__(self) -> str:
|
362
|
+
return f"MaxExpectedValueBettingStrategy(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
|
363
|
+
|
353
364
|
|
354
|
-
class
|
365
|
+
class BinaryKellyBettingStrategy(BettingStrategy):
|
355
366
|
def __init__(
|
356
367
|
self,
|
357
368
|
max_position_amount: USD,
|
358
369
|
max_price_impact: float | None = None,
|
359
370
|
take_profit: bool = True,
|
371
|
+
force_simplified_calculation: bool = False,
|
360
372
|
):
|
361
373
|
super().__init__(take_profit=take_profit)
|
362
374
|
self.max_position_amount = max_position_amount
|
363
375
|
self.max_price_impact = max_price_impact
|
376
|
+
self.force_simplified_calculation = force_simplified_calculation
|
364
377
|
|
365
378
|
@property
|
366
379
|
def maximum_possible_bet_amount(self) -> USD:
|
367
380
|
return self.max_position_amount
|
368
381
|
|
369
|
-
@staticmethod
|
370
382
|
def get_kelly_bet(
|
383
|
+
self,
|
371
384
|
market: AgentMarket,
|
372
|
-
max_bet_amount: USD,
|
373
385
|
direction: OutcomeStr,
|
374
386
|
other_direction: OutcomeStr,
|
375
387
|
answer: CategoricalProbabilisticAnswer,
|
376
388
|
override_p_yes: float | None = None,
|
377
|
-
) ->
|
389
|
+
) -> BinaryKellyBet:
|
378
390
|
estimated_p_yes = (
|
379
391
|
answer.probability_for_market_outcome(direction)
|
380
392
|
if not override_p_yes
|
381
393
|
else override_p_yes
|
382
394
|
)
|
383
395
|
|
384
|
-
if
|
385
|
-
# use Kelly simple, since Kelly full only supports 2 outcomes
|
386
|
-
|
396
|
+
if market.outcome_token_pool is None or self.force_simplified_calculation:
|
387
397
|
kelly_bet = get_kelly_bet_simplified(
|
388
|
-
max_bet=market.get_usd_in_token(
|
398
|
+
max_bet=market.get_usd_in_token(self.max_position_amount),
|
389
399
|
market_p_yes=market.probability_for_market_outcome(direction),
|
390
400
|
estimated_p_yes=estimated_p_yes,
|
391
401
|
confidence=answer.confidence,
|
392
402
|
)
|
393
403
|
else:
|
394
|
-
# We consider only binary markets, since the Kelly strategy is not yet implemented
|
395
|
-
# for markets with more than 2 outcomes (https://github.com/gnosis/prediction-market-agent-tooling/issues/671).
|
396
404
|
direction_to_bet_pool_size = market.get_outcome_token_pool_by_outcome(
|
397
405
|
direction
|
398
406
|
)
|
@@ -403,7 +411,7 @@ class KellyBettingStrategy(BettingStrategy):
|
|
403
411
|
yes_outcome_pool_size=direction_to_bet_pool_size,
|
404
412
|
no_outcome_pool_size=other_direction_pool_size,
|
405
413
|
estimated_p_yes=estimated_p_yes,
|
406
|
-
max_bet=market.get_usd_in_token(
|
414
|
+
max_bet=market.get_usd_in_token(self.max_position_amount),
|
407
415
|
confidence=answer.confidence,
|
408
416
|
fees=market.fees,
|
409
417
|
)
|
@@ -416,7 +424,7 @@ class KellyBettingStrategy(BettingStrategy):
|
|
416
424
|
market: AgentMarket,
|
417
425
|
) -> list[Trade]:
|
418
426
|
# We consider the p_yes as the direction with highest probability.
|
419
|
-
direction =
|
427
|
+
direction = CategoricalMaxAccuracyBettingStrategy.calculate_direction(
|
420
428
|
market, answer
|
421
429
|
)
|
422
430
|
# We get the first direction which is != direction.
|
@@ -426,7 +434,6 @@ class KellyBettingStrategy(BettingStrategy):
|
|
426
434
|
|
427
435
|
kelly_bet = self.get_kelly_bet(
|
428
436
|
market=market,
|
429
|
-
max_bet_amount=self.max_position_amount,
|
430
437
|
direction=direction,
|
431
438
|
other_direction=other_direction,
|
432
439
|
answer=answer,
|
@@ -436,15 +443,21 @@ class KellyBettingStrategy(BettingStrategy):
|
|
436
443
|
if self.max_price_impact:
|
437
444
|
# Adjust amount
|
438
445
|
max_price_impact_bet_amount = self.calculate_bet_amount_for_price_impact(
|
439
|
-
market,
|
446
|
+
market,
|
447
|
+
kelly_bet.size,
|
448
|
+
direction=direction,
|
449
|
+
max_price_impact=self.max_price_impact,
|
440
450
|
)
|
441
451
|
|
442
452
|
# We just don't want Kelly size to extrapolate price_impact - hence we take the min.
|
443
453
|
kelly_bet_size = min(kelly_bet.size, max_price_impact_bet_amount)
|
444
454
|
|
445
455
|
bet_outcome = direction if kelly_bet.direction else other_direction
|
456
|
+
|
446
457
|
amounts = {
|
447
|
-
bet_outcome:
|
458
|
+
bet_outcome: BettingStrategy.cap_to_profitable_bet_amount(
|
459
|
+
market, market.get_token_in_usd(kelly_bet_size), bet_outcome
|
460
|
+
),
|
448
461
|
}
|
449
462
|
target_position = Position(market_id=market.id, amounts_current=amounts)
|
450
463
|
trades = self._build_rebalance_trades_from_positions(
|
@@ -452,8 +465,8 @@ class KellyBettingStrategy(BettingStrategy):
|
|
452
465
|
)
|
453
466
|
return trades
|
454
467
|
|
468
|
+
@staticmethod
|
455
469
|
def calculate_price_impact_for_bet_amount(
|
456
|
-
self,
|
457
470
|
outcome_idx: int,
|
458
471
|
bet_amount: CollateralToken,
|
459
472
|
pool_balances: list[OutcomeWei],
|
@@ -471,29 +484,33 @@ class KellyBettingStrategy(BettingStrategy):
|
|
471
484
|
price_impact = (actual_price - expected_price) / expected_price
|
472
485
|
return price_impact
|
473
486
|
|
487
|
+
@staticmethod
|
474
488
|
def calculate_bet_amount_for_price_impact(
|
475
|
-
|
489
|
+
market: AgentMarket,
|
490
|
+
kelly_bet_size: CollateralToken,
|
491
|
+
direction: OutcomeStr,
|
492
|
+
max_price_impact: float,
|
476
493
|
) -> CollateralToken:
|
477
494
|
def calculate_price_impact_deviation_from_target_price_impact(
|
478
|
-
|
495
|
+
bet_amount_collateral: float, # Needs to be float because it's used in minimize_scalar internally.
|
479
496
|
) -> float:
|
480
497
|
outcome_idx = market.get_outcome_index(direction)
|
481
|
-
price_impact =
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
498
|
+
price_impact = (
|
499
|
+
BinaryKellyBettingStrategy.calculate_price_impact_for_bet_amount(
|
500
|
+
outcome_idx=outcome_idx,
|
501
|
+
bet_amount=CollateralToken(bet_amount_collateral),
|
502
|
+
pool_balances=pool_balances,
|
503
|
+
fees=market.fees,
|
504
|
+
)
|
486
505
|
)
|
487
506
|
# We return abs for the algorithm to converge to 0 instead of the min (and possibly negative) value.
|
488
|
-
|
489
|
-
max_price_impact = check_not_none(self.max_price_impact)
|
490
507
|
return abs(price_impact - max_price_impact)
|
491
508
|
|
492
509
|
if not market.outcome_token_pool:
|
493
510
|
logger.warning(
|
494
511
|
"Market outcome_token_pool is None, cannot calculate bet amount"
|
495
512
|
)
|
496
|
-
return
|
513
|
+
return kelly_bet_size
|
497
514
|
|
498
515
|
pool_balances = [i.as_outcome_wei for i in market.outcome_token_pool.values()]
|
499
516
|
# stay float for compatibility with `minimize_scalar`
|
@@ -504,13 +521,13 @@ class KellyBettingStrategy(BettingStrategy):
|
|
504
521
|
calculate_price_impact_deviation_from_target_price_impact,
|
505
522
|
bounds=(0, 1000 * total_pool_balance),
|
506
523
|
method="bounded",
|
507
|
-
tol=1e-
|
524
|
+
tol=1e-13,
|
508
525
|
options={"maxiter": 10000},
|
509
526
|
)
|
510
527
|
return CollateralToken(optimized_bet_amount.x)
|
511
528
|
|
512
529
|
def __repr__(self) -> str:
|
513
|
-
return f"{self.__class__.__name__}(
|
530
|
+
return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, max_price_impact={self.max_price_impact}, take_profit={self.take_profit}, force_simplified_calculation={self.force_simplified_calculation})"
|
514
531
|
|
515
532
|
|
516
533
|
class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
|
@@ -526,40 +543,29 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
|
|
526
543
|
def maximum_possible_bet_amount(self) -> USD:
|
527
544
|
return self.max_position_amount
|
528
545
|
|
529
|
-
def adjust_bet_amount(
|
530
|
-
self, existing_position: ExistingPosition | None, market: AgentMarket
|
531
|
-
) -> USD:
|
532
|
-
existing_position_total_amount = (
|
533
|
-
existing_position.total_amount_current if existing_position else USD(0)
|
534
|
-
)
|
535
|
-
return self.max_position_amount + existing_position_total_amount
|
536
|
-
|
537
546
|
def calculate_trades(
|
538
547
|
self,
|
539
548
|
existing_position: ExistingPosition | None,
|
540
549
|
answer: CategoricalProbabilisticAnswer,
|
541
550
|
market: AgentMarket,
|
542
551
|
) -> list[Trade]:
|
543
|
-
adjusted_bet_amount_usd = self.adjust_bet_amount(existing_position, market)
|
544
|
-
|
545
552
|
outcome = get_most_probable_outcome(answer.probabilities)
|
546
553
|
|
547
|
-
direction =
|
554
|
+
direction = CategoricalMaxAccuracyBettingStrategy.calculate_direction(
|
548
555
|
market, answer
|
549
556
|
)
|
550
557
|
# We get the first direction which is != direction.
|
551
|
-
other_direction = (
|
552
|
-
|
553
|
-
outcomes=market.outcomes, direction=direction
|
554
|
-
)
|
558
|
+
other_direction = CategoricalMaxAccuracyBettingStrategy.get_other_direction(
|
559
|
+
outcomes=market.outcomes, direction=direction
|
555
560
|
)
|
556
561
|
|
557
562
|
# We ignore the direction nudge given by Kelly, hence we assume we have a perfect prediction.
|
558
563
|
estimated_p_yes = 1.0
|
559
564
|
|
560
|
-
kelly_bet =
|
565
|
+
kelly_bet = BinaryKellyBettingStrategy(
|
566
|
+
max_position_amount=self.max_position_amount
|
567
|
+
).get_kelly_bet(
|
561
568
|
market=market,
|
562
|
-
max_bet_amount=adjusted_bet_amount_usd,
|
563
569
|
direction=direction,
|
564
570
|
other_direction=other_direction,
|
565
571
|
answer=answer,
|
@@ -581,4 +587,118 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
|
|
581
587
|
return trades
|
582
588
|
|
583
589
|
def __repr__(self) -> str:
|
584
|
-
return f"{self.__class__.__name__}(
|
590
|
+
return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
|
591
|
+
|
592
|
+
|
593
|
+
class CategoricalKellyBettingStrategy(BettingStrategy):
|
594
|
+
def __init__(
|
595
|
+
self,
|
596
|
+
max_position_amount: USD,
|
597
|
+
max_price_impact: float | None,
|
598
|
+
allow_multiple_bets: bool,
|
599
|
+
allow_shorting: bool,
|
600
|
+
multicategorical: bool,
|
601
|
+
take_profit: bool = True,
|
602
|
+
force_simplified_calculation: bool = False,
|
603
|
+
):
|
604
|
+
super().__init__(take_profit=take_profit)
|
605
|
+
self.max_position_amount = max_position_amount
|
606
|
+
self.max_price_impact = max_price_impact
|
607
|
+
self.allow_multiple_bets = allow_multiple_bets
|
608
|
+
self.allow_shorting = allow_shorting
|
609
|
+
self.multicategorical = multicategorical
|
610
|
+
self.force_simplified_calculation = force_simplified_calculation
|
611
|
+
|
612
|
+
@property
|
613
|
+
def maximum_possible_bet_amount(self) -> USD:
|
614
|
+
return self.max_position_amount
|
615
|
+
|
616
|
+
def get_kelly_bets(
|
617
|
+
self,
|
618
|
+
market: AgentMarket,
|
619
|
+
max_bet_amount: USD,
|
620
|
+
answer: CategoricalProbabilisticAnswer,
|
621
|
+
) -> list[CategoricalKellyBet]:
|
622
|
+
max_bet = market.get_usd_in_token(max_bet_amount)
|
623
|
+
|
624
|
+
if market.outcome_token_pool is None or self.force_simplified_calculation:
|
625
|
+
kelly_bets = get_kelly_bets_categorical_simplified(
|
626
|
+
market_probabilities=[market.probabilities[o] for o in market.outcomes],
|
627
|
+
estimated_probabilities=[
|
628
|
+
answer.probability_for_market_outcome(o) for o in market.outcomes
|
629
|
+
],
|
630
|
+
confidence=answer.confidence,
|
631
|
+
max_bet=max_bet,
|
632
|
+
fees=market.fees,
|
633
|
+
allow_multiple_bets=self.allow_multiple_bets,
|
634
|
+
allow_shorting=self.allow_shorting,
|
635
|
+
)
|
636
|
+
|
637
|
+
else:
|
638
|
+
kelly_bets = get_kelly_bets_categorical_full(
|
639
|
+
outcome_pool_sizes=[
|
640
|
+
market.outcome_token_pool[o] for o in market.outcomes
|
641
|
+
],
|
642
|
+
estimated_probabilities=[
|
643
|
+
answer.probability_for_market_outcome(o) for o in market.outcomes
|
644
|
+
],
|
645
|
+
confidence=answer.confidence,
|
646
|
+
max_bet=max_bet,
|
647
|
+
fees=market.fees,
|
648
|
+
allow_multiple_bets=self.allow_multiple_bets,
|
649
|
+
allow_shorting=self.allow_shorting,
|
650
|
+
multicategorical=self.multicategorical,
|
651
|
+
)
|
652
|
+
|
653
|
+
return kelly_bets
|
654
|
+
|
655
|
+
def calculate_trades(
|
656
|
+
self,
|
657
|
+
existing_position: ExistingPosition | None,
|
658
|
+
answer: CategoricalProbabilisticAnswer,
|
659
|
+
market: AgentMarket,
|
660
|
+
) -> list[Trade]:
|
661
|
+
kelly_bets = self.get_kelly_bets(
|
662
|
+
market=market,
|
663
|
+
max_bet_amount=self.max_position_amount,
|
664
|
+
answer=answer,
|
665
|
+
)
|
666
|
+
|
667
|
+
# TODO: Allow shorting in BettingStrategy._build_rebalance_trades_from_positions.
|
668
|
+
# In binary implementation, we simply flip the direction in case of negative bet, for categorical outcome, we need to implement shorting.
|
669
|
+
kelly_bets = [bet for bet in kelly_bets if bet.size > 0]
|
670
|
+
if not kelly_bets:
|
671
|
+
return []
|
672
|
+
|
673
|
+
# TODO: Allow betting on multiple outcomes.
|
674
|
+
# Categorical kelly could suggest to bet on multiple outcomes, but we only consider the first one for now (limitation of BettingStrategy `trades` creation).
|
675
|
+
# Also, this could maybe work for multi-categorical markets as well, but it wasn't benchmarked for it.
|
676
|
+
best_kelly_bet = max(kelly_bets, key=lambda x: abs(x.size))
|
677
|
+
|
678
|
+
if self.max_price_impact:
|
679
|
+
# Adjust amount
|
680
|
+
max_price_impact_bet_amount = (
|
681
|
+
BinaryKellyBettingStrategy.calculate_bet_amount_for_price_impact(
|
682
|
+
market,
|
683
|
+
best_kelly_bet.size,
|
684
|
+
direction=market.get_outcome_str(best_kelly_bet.index),
|
685
|
+
max_price_impact=self.max_price_impact,
|
686
|
+
)
|
687
|
+
)
|
688
|
+
# We just don't want Kelly size to extrapolate price_impact - hence we take the min.
|
689
|
+
best_kelly_bet.size = min(best_kelly_bet.size, max_price_impact_bet_amount)
|
690
|
+
|
691
|
+
amounts = {
|
692
|
+
market.outcomes[best_kelly_bet.index]: market.get_token_in_usd(
|
693
|
+
best_kelly_bet.size
|
694
|
+
),
|
695
|
+
}
|
696
|
+
target_position = Position(market_id=market.id, amounts_current=amounts)
|
697
|
+
trades = self._build_rebalance_trades_from_positions(
|
698
|
+
existing_position, target_position, market=market
|
699
|
+
)
|
700
|
+
|
701
|
+
return trades
|
702
|
+
|
703
|
+
def __repr__(self) -> str:
|
704
|
+
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}, force_simplified_calculation={self.force_simplified_calculation})"
|
@@ -2,7 +2,7 @@ import typing as t
|
|
2
2
|
|
3
3
|
from prediction_market_agent_tooling.config import APIKeys
|
4
4
|
from prediction_market_agent_tooling.deploy.betting_strategy import (
|
5
|
-
|
5
|
+
BinaryKellyBettingStrategy,
|
6
6
|
TradeType,
|
7
7
|
)
|
8
8
|
from prediction_market_agent_tooling.gtypes import USD
|
@@ -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 =
|
96
|
+
strategy = BinaryKellyBettingStrategy(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),
|
@@ -153,6 +153,23 @@ class AgentMarket(BaseModel):
|
|
153
153
|
if "fees" not in data and "fee" in data:
|
154
154
|
data["fees"] = MarketFees(absolute=0.0, bet_proportion=data["fee"])
|
155
155
|
del data["fee"]
|
156
|
+
# Backward compatibility for older `AgentMarket` without `probabilities`.
|
157
|
+
if "probabilities" not in data and "current_p_yes" in data:
|
158
|
+
yes_outcome = data["outcomes"][
|
159
|
+
[o.lower() for o in data["outcomes"]].index(
|
160
|
+
YES_OUTCOME_LOWERCASE_IDENTIFIER
|
161
|
+
)
|
162
|
+
]
|
163
|
+
no_outcome = data["outcomes"][
|
164
|
+
[o.lower() for o in data["outcomes"]].index(
|
165
|
+
NO_OUTCOME_LOWERCASE_IDENTIFIER
|
166
|
+
)
|
167
|
+
]
|
168
|
+
data["probabilities"] = {
|
169
|
+
yes_outcome: data["current_p_yes"],
|
170
|
+
no_outcome: 1 - data["current_p_yes"],
|
171
|
+
}
|
172
|
+
del data["current_p_yes"]
|
156
173
|
return data
|
157
174
|
|
158
175
|
def market_outcome_for_probability_key(
|
@@ -37,6 +37,10 @@ def store_trades(
|
|
37
37
|
logger.warning(f"No prediction for market {market_id}, not storing anything.")
|
38
38
|
return None
|
39
39
|
|
40
|
+
logger.info(
|
41
|
+
f"Storing trades for market {market_id}, with outcomes {outcomes}, {traded_market=}."
|
42
|
+
)
|
43
|
+
|
40
44
|
probabilities = traded_market.answer.probabilities
|
41
45
|
if not probabilities:
|
42
46
|
logger.info("Skipping this since no probabilities available.")
|
@@ -56,9 +60,7 @@ def store_trades(
|
|
56
60
|
ipfs_hash_decoded = ipfscidv0_to_byte32(ipfs_hash)
|
57
61
|
|
58
62
|
# tx_hashes must be list of bytes32 (see Solidity contract).
|
59
|
-
tx_hashes = [
|
60
|
-
HexBytes(HexStr(i.id)) for i in traded_market.trades if i.id is not None
|
61
|
-
]
|
63
|
+
tx_hashes = [HexBytes(HexStr(i.id)) for i in traded_market.trades]
|
62
64
|
|
63
65
|
# Dune dashboard expects the probs to be in the same order as on the market.
|
64
66
|
probabilities_converted = [
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from enum import Enum
|
2
|
-
from typing import Annotated, Sequence
|
2
|
+
from typing import Annotated, Any, Sequence
|
3
3
|
|
4
|
-
from pydantic import BaseModel, BeforeValidator, computed_field
|
4
|
+
from pydantic import BaseModel, BeforeValidator, computed_field, model_validator
|
5
5
|
|
6
6
|
from prediction_market_agent_tooling.deploy.constants import (
|
7
7
|
DOWN_OUTCOME_LOWERCASE_IDENTIFIER,
|
@@ -157,6 +157,15 @@ class CategoricalProbabilisticAnswer(BaseModel):
|
|
157
157
|
confidence: float
|
158
158
|
reasoning: str | None = None
|
159
159
|
|
160
|
+
@model_validator(mode="before")
|
161
|
+
@classmethod
|
162
|
+
def _model_validator(cls, data: Any) -> Any:
|
163
|
+
if "p_yes" in data:
|
164
|
+
return CategoricalProbabilisticAnswer.from_probabilistic_answer(
|
165
|
+
ProbabilisticAnswer.model_validate(data)
|
166
|
+
).model_dump()
|
167
|
+
return data
|
168
|
+
|
160
169
|
@property
|
161
170
|
def probable_resolution(self) -> Resolution:
|
162
171
|
most_likely_outcome = max(
|
@@ -290,9 +299,20 @@ class Trade(BaseModel):
|
|
290
299
|
outcome: OutcomeStr
|
291
300
|
amount: USD
|
292
301
|
|
302
|
+
@model_validator(mode="before")
|
303
|
+
@classmethod
|
304
|
+
def _model_validator(cls, data: Any) -> Any:
|
305
|
+
if isinstance(data["outcome"], bool):
|
306
|
+
data["outcome"] = (
|
307
|
+
YES_OUTCOME_LOWERCASE_IDENTIFIER
|
308
|
+
if data["outcome"]
|
309
|
+
else NO_OUTCOME_LOWERCASE_IDENTIFIER
|
310
|
+
)
|
311
|
+
return data
|
312
|
+
|
293
313
|
|
294
314
|
class PlacedTrade(Trade):
|
295
|
-
id: str
|
315
|
+
id: str
|
296
316
|
|
297
317
|
@staticmethod
|
298
318
|
def from_trade(trade: Trade, id: str) -> "PlacedTrade":
|
@@ -601,7 +601,12 @@ class OmenBet(BaseModel):
|
|
601
601
|
created_time=self.creation_datetime,
|
602
602
|
market_question=self.title,
|
603
603
|
market_id=self.fpmm.id,
|
604
|
-
market_outcome=self.fpmm.outcomes[
|
604
|
+
market_outcome=self.fpmm.outcomes[
|
605
|
+
check_not_none(
|
606
|
+
self.fpmm.answer_index,
|
607
|
+
"Should not be None if `is_resolved_with_valid_answer`.",
|
608
|
+
)
|
609
|
+
],
|
605
610
|
resolved_time=check_not_none(self.fpmm.finalized_datetime),
|
606
611
|
profit=self.get_profit(),
|
607
612
|
)
|
@@ -3,6 +3,7 @@ from collections import defaultdict
|
|
3
3
|
from datetime import timedelta
|
4
4
|
|
5
5
|
import tenacity
|
6
|
+
from pydantic import BaseModel
|
6
7
|
from tqdm import tqdm
|
7
8
|
from web3 import Web3
|
8
9
|
|
@@ -1323,20 +1324,34 @@ def send_keeping_token_to_eoa_xdai(
|
|
1323
1324
|
)
|
1324
1325
|
|
1325
1326
|
|
1326
|
-
|
1327
|
+
class BuyOutcomeResult(BaseModel):
|
1328
|
+
outcome_tokens_received: OutcomeToken
|
1329
|
+
new_pool_balances: list[OutcomeToken]
|
1330
|
+
|
1331
|
+
|
1332
|
+
def calculate_buy_outcome_token(
|
1327
1333
|
investment_amount: CollateralToken,
|
1328
1334
|
outcome_index: int,
|
1329
1335
|
pool_balances: list[OutcomeToken],
|
1330
1336
|
fees: MarketFees,
|
1331
|
-
) ->
|
1337
|
+
) -> BuyOutcomeResult:
|
1332
1338
|
"""
|
1333
1339
|
Calculates the amount of outcome tokens received for a given investment
|
1340
|
+
and returns the new pool balances after the purchase.
|
1334
1341
|
|
1335
1342
|
Taken from https://github.com/gnosis/conditional-tokens-market-makers/blob/6814c0247c745680bb13298d4f0dd7f5b574d0db/contracts/FixedProductMarketMaker.sol#L264
|
1336
1343
|
"""
|
1337
1344
|
if outcome_index >= len(pool_balances):
|
1338
1345
|
raise ValueError("invalid outcome index")
|
1339
1346
|
|
1347
|
+
new_pool_balances = pool_balances.copy()
|
1348
|
+
|
1349
|
+
if investment_amount == 0:
|
1350
|
+
return BuyOutcomeResult(
|
1351
|
+
outcome_tokens_received=OutcomeToken(0),
|
1352
|
+
new_pool_balances=new_pool_balances,
|
1353
|
+
)
|
1354
|
+
|
1340
1355
|
investment_amount_minus_fees = fees.get_after_fees(investment_amount)
|
1341
1356
|
investment_amount_minus_fees_as_ot = OutcomeToken(
|
1342
1357
|
investment_amount_minus_fees.value
|
@@ -1348,17 +1363,39 @@ def get_buy_outcome_token_amount(
|
|
1348
1363
|
# Calculate the ending balance considering all other outcomes
|
1349
1364
|
for i, pool_balance in enumerate(pool_balances):
|
1350
1365
|
if i != outcome_index:
|
1351
|
-
|
1366
|
+
new_pool_balances[i] = pool_balance + investment_amount_minus_fees_as_ot
|
1352
1367
|
ending_outcome_balance = OutcomeToken(
|
1353
|
-
(ending_outcome_balance * pool_balance /
|
1368
|
+
(ending_outcome_balance * pool_balance / new_pool_balances[i])
|
1354
1369
|
)
|
1355
1370
|
|
1371
|
+
# Update the bought outcome's pool balance
|
1372
|
+
new_pool_balances[outcome_index] = ending_outcome_balance
|
1373
|
+
|
1356
1374
|
if ending_outcome_balance <= 0:
|
1357
1375
|
raise ValueError("must have non-zero balances")
|
1358
1376
|
|
1359
|
-
|
1377
|
+
outcome_tokens_received = (
|
1360
1378
|
buy_token_pool_balance
|
1361
1379
|
+ investment_amount_minus_fees_as_ot
|
1362
1380
|
- ending_outcome_balance
|
1363
1381
|
)
|
1364
|
-
|
1382
|
+
|
1383
|
+
return BuyOutcomeResult(
|
1384
|
+
outcome_tokens_received=outcome_tokens_received,
|
1385
|
+
new_pool_balances=new_pool_balances,
|
1386
|
+
)
|
1387
|
+
|
1388
|
+
|
1389
|
+
def get_buy_outcome_token_amount(
|
1390
|
+
investment_amount: CollateralToken,
|
1391
|
+
outcome_index: int,
|
1392
|
+
pool_balances: list[OutcomeToken],
|
1393
|
+
fees: MarketFees,
|
1394
|
+
) -> OutcomeToken:
|
1395
|
+
result = calculate_buy_outcome_token(
|
1396
|
+
investment_amount=investment_amount,
|
1397
|
+
outcome_index=outcome_index,
|
1398
|
+
pool_balances=pool_balances,
|
1399
|
+
fees=fees,
|
1400
|
+
)
|
1401
|
+
return result.outcome_tokens_received
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import typing as t
|
2
|
+
from datetime import timedelta
|
2
3
|
from enum import Enum
|
3
4
|
from urllib.parse import urljoin
|
4
5
|
|
@@ -49,7 +50,7 @@ def get_polymarkets_with_pagination(
|
|
49
50
|
Binary markets have len(model.markets) == 1.
|
50
51
|
Categorical markets have len(model.markets) > 1
|
51
52
|
"""
|
52
|
-
client: httpx.Client = HttpxCachedClient(ttl=60).get_client()
|
53
|
+
client: httpx.Client = HttpxCachedClient(ttl=timedelta(seconds=60)).get_client()
|
53
54
|
all_markets: list[PolymarketGammaResponseDataItem] = []
|
54
55
|
offset = 0
|
55
56
|
remaining = limit
|
@@ -82,6 +83,9 @@ def get_polymarkets_with_pagination(
|
|
82
83
|
|
83
84
|
markets_to_add = []
|
84
85
|
for m in market_response.data:
|
86
|
+
# Some Polymarket markets are missing the markets field
|
87
|
+
if m.markets is None:
|
88
|
+
continue
|
85
89
|
if excluded_questions and m.title in excluded_questions:
|
86
90
|
continue
|
87
91
|
|
@@ -94,14 +98,16 @@ def get_polymarkets_with_pagination(
|
|
94
98
|
]:
|
95
99
|
continue
|
96
100
|
|
97
|
-
if created_after and created_after > m.startDate:
|
101
|
+
if not m.startDate or (created_after and created_after > m.startDate):
|
98
102
|
continue
|
99
103
|
|
100
104
|
markets_to_add.append(m)
|
101
105
|
|
102
106
|
if only_binary:
|
103
107
|
markets_to_add = [
|
104
|
-
market
|
108
|
+
market
|
109
|
+
for market in markets_to_add
|
110
|
+
if market.markets is not None and len(market.markets) == 1
|
105
111
|
]
|
106
112
|
|
107
113
|
# Add the markets from this batch to our results
|