Skip to content

Commit

Permalink
338 change default betting strategy from amount == tiny bet direction…
Browse files Browse the repository at this point in the history
… == yes if p yes 05 to use kelly bet (#352)

* Refactored tiny_bet_amount to strategy

* Updating deps

* Refactoring betting strategies

* Added default strategies

* Refactoring betting strategies

* Added lock

* Bump version

* Delete run_all.sh

* Fixing tests

* Updated GNOSIS_RPC

* Added exception message pattern to be retried

* Refactored exception message

* Added ape as dependency again

* Implementing PR comments (1)

* Update prediction_market_agent_tooling/deploy/agent.py

Co-authored-by: Peter Jung <peter@jung.ninja>

* Update prediction_market_agent_tooling/deploy/betting_strategy.py

Co-authored-by: Peter Jung <peter@jung.ninja>

* Update prediction_market_agent_tooling/deploy/betting_strategy.py

Co-authored-by: Peter Jung <peter@jung.ninja>

* Refactored get_tiny_bet_amount

* Refactored Answer into ProbabilisticAnswer, since direction removed

* Updated lock

* Small changes after merge

* Added calculator for directions in betting strategies

* Removed Answer

* Added comment to calculator classes | renaming for clarity

* Removed unnecessary share calculations, replacing that with clever math

* Updated lock file

* Update prediction_market_agent_tooling/deploy/betting_strategy.py

Co-authored-by: Evan Griffiths <56087052+evangriffiths@users.noreply.github.com>

* Update prediction_market_agent_tooling/deploy/betting_strategy.py

Co-authored-by: Evan Griffiths <56087052+evangriffiths@users.noreply.github.com>

* Implemented PR changes

* Update prediction_market_agent_tooling/deploy/betting_strategy.py

Co-authored-by: Evan Griffiths <56087052+evangriffiths@users.noreply.github.com>

* Fixing CI

* Added configurable bet_amount

* Implemented PR changes

---------

Co-authored-by: Peter Jung <peter@jung.ninja>
Co-authored-by: Evan Griffiths <56087052+evangriffiths@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 27, 2024
1 parent 34fd87f commit 5a57423
Show file tree
Hide file tree
Showing 12 changed files with 438 additions and 358 deletions.
541 changes: 264 additions & 277 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions prediction_market_agent_tooling/benchmark/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ def predict(self, market_question: str) -> Prediction:
p_yes, confidence = random.random(), random.random()
return Prediction(
outcome_prediction=OutcomePrediction(
decision=p_yes > 0.5,
p_yes=Probability(p_yes),
confidence=confidence,
info_utility=None,
Expand All @@ -111,7 +110,6 @@ def predict(self, market_question: str) -> Prediction:
p_yes, confidence = 1.0 if self.fixed_answer else 0.0, 1.0
return Prediction(
outcome_prediction=OutcomePrediction(
decision=self.fixed_answer,
p_yes=Probability(p_yes),
confidence=confidence,
info_utility=None,
Expand Down
8 changes: 5 additions & 3 deletions prediction_market_agent_tooling/benchmark/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

from pydantic import BaseModel

from prediction_market_agent_tooling.deploy.agent import Answer
from prediction_market_agent_tooling.markets.data_models import Resolution
from prediction_market_agent_tooling.markets.data_models import (
ProbabilisticAnswer,
Resolution,
)


class OutcomePrediction(Answer):
class OutcomePrediction(ProbabilisticAnswer):
info_utility: t.Optional[float]

@property
Expand Down
62 changes: 35 additions & 27 deletions prediction_market_agent_tooling/deploy/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from typing_extensions import Annotated

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.deploy.betting_strategy import (
BettingStrategy,
MaxAccuracyBettingStrategy,
)
from prediction_market_agent_tooling.deploy.constants import (
MARKET_TYPE_KEY,
REPOSITORY_KEY,
Expand All @@ -25,14 +29,19 @@
gcp_function_is_active,
gcp_resolve_api_keys_secrets,
)
from prediction_market_agent_tooling.gtypes import Probability, xDai, xdai_type
from prediction_market_agent_tooling.gtypes import xDai, xdai_type
from prediction_market_agent_tooling.loggers import logger
from prediction_market_agent_tooling.markets.agent_market import (
AgentMarket,
FilterBy,
SortBy,
)
from prediction_market_agent_tooling.markets.data_models import BetAmount
from prediction_market_agent_tooling.markets.data_models import (
BetAmount,
ProbabilisticAnswer,
TokenAmount,
TokenAmountAndDirection,
)
from prediction_market_agent_tooling.markets.markets import (
MarketType,
have_bet_on_market_since,
Expand Down Expand Up @@ -95,19 +104,8 @@ class OutOfFundsError(ValueError):
pass


class Answer(BaseModel):
decision: Decision # Warning: p_yes > 0.5 doesn't necessarily mean decision is True! For example, if our p_yes is 55%, but market's p_yes is 80%, then it might be profitable to bet on False.
p_yes: Probability
confidence: float
reasoning: str | None = None

@property
def p_no(self) -> Probability:
return Probability(1 - self.p_yes)


class ProcessedMarket(BaseModel):
answer: Answer
answer: ProbabilisticAnswer
amount: BetAmount


Expand Down Expand Up @@ -280,6 +278,7 @@ class DeployableTraderAgent(DeployableAgent):
bet_on_n_markets_per_run: int = 1
min_required_balance_to_operate: xDai | None = xdai_type(1)
min_balance_to_keep_in_native_currency: xDai | None = xdai_type(0.1)
strategy: BettingStrategy = MaxAccuracyBettingStrategy()

def __init__(
self,
Expand All @@ -295,7 +294,7 @@ def initialize_langfuse(self) -> None:
self.have_bet_on_market_since = observe()(self.have_bet_on_market_since) # type: ignore[method-assign]
self.verify_market = observe()(self.verify_market) # type: ignore[method-assign]
self.answer_binary_market = observe()(self.answer_binary_market) # type: ignore[method-assign]
self.calculate_bet_amount = observe()(self.calculate_bet_amount) # type: ignore[method-assign]
self.calculate_bet_amount_and_direction = observe()(self.calculate_bet_amount_and_direction) # type: ignore[method-assign]
self.process_market = observe()(self.process_market) # type: ignore[method-assign]

def update_langfuse_trace_by_market(
Expand All @@ -311,6 +310,18 @@ def update_langfuse_trace_by_market(
},
)

def calculate_bet_amount_and_direction(
self, answer: ProbabilisticAnswer, market: AgentMarket
) -> TokenAmountAndDirection:
amount_and_direction = self.strategy.calculate_bet_amount_and_direction(
answer, market
)
if amount_and_direction.currency != market.currency:
raise ValueError(
f"Currency mismatch. Strategy yields {amount_and_direction.currency}, market has currency {market.currency}"
)
return amount_and_direction

def update_langfuse_trace_by_processed_market(
self, market_type: MarketType, processed_market: ProcessedMarket | None
) -> None:
Expand Down Expand Up @@ -360,18 +371,12 @@ def verify_market(self, market_type: MarketType, market: AgentMarket) -> bool:

return True

def answer_binary_market(self, market: AgentMarket) -> Answer | None:
def answer_binary_market(self, market: AgentMarket) -> ProbabilisticAnswer | None:
"""
Answer the binary market. This method must be implemented by the subclass.
"""
raise NotImplementedError("This method must be implemented by the subclass")

def calculate_bet_amount(self, answer: Answer, market: AgentMarket) -> BetAmount:
"""
Calculate the bet amount. By default, it returns the minimum bet amount.
"""
return market.get_tiny_bet_amount()

def get_markets(
self,
market_type: MarketType,
Expand Down Expand Up @@ -408,22 +413,25 @@ def process_market(
self.update_langfuse_trace_by_processed_market(market_type, None)
return None

amount = self.calculate_bet_amount(answer, market)
amount_and_direction = self.calculate_bet_amount_and_direction(answer, market)

if self.place_bet:
logger.info(
f"Placing bet on {market} with result {answer} and amount {amount}"
f"Placing bet on {market} with direction {amount_and_direction.direction} and amount {amount_and_direction.amount}"
)
market.place_bet(
amount=amount,
outcome=answer.decision,
amount=TokenAmount(
amount=amount_and_direction.amount,
currency=amount_and_direction.currency,
),
outcome=amount_and_direction.direction,
)

self.after_process_market(market_type, market)

processed_market = ProcessedMarket(
answer=answer,
amount=amount,
amount=amount_and_direction,
)
self.update_langfuse_trace_by_processed_market(market_type, processed_market)

Expand Down
11 changes: 5 additions & 6 deletions prediction_market_agent_tooling/deploy/agent_example.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import random

from prediction_market_agent_tooling.deploy.agent import (
Answer,
DeployableTraderAgent,
Probability,
ProbabilisticAnswer,
)
from prediction_market_agent_tooling.gtypes import Probability
from prediction_market_agent_tooling.markets.agent_market import AgentMarket
from prediction_market_agent_tooling.markets.markets import MarketType

Expand All @@ -13,16 +13,15 @@ class DeployableCoinFlipAgent(DeployableTraderAgent):
def verify_market(self, market_type: MarketType, market: AgentMarket) -> bool:
return True

def answer_binary_market(self, market: AgentMarket) -> Answer | None:
def answer_binary_market(self, market: AgentMarket) -> ProbabilisticAnswer | None:
decision = random.choice([True, False])
return Answer(
decision=decision,
return ProbabilisticAnswer(
p_yes=Probability(float(decision)),
confidence=0.5,
reasoning="I flipped a coin to decide.",
)


class DeployableAlwaysRaiseAgent(DeployableTraderAgent):
def answer_binary_market(self, market: AgentMarket) -> Answer | None:
def answer_binary_market(self, market: AgentMarket) -> ProbabilisticAnswer | None:
raise RuntimeError("I always raise!")
62 changes: 62 additions & 0 deletions prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from abc import ABC, abstractmethod

from prediction_market_agent_tooling.markets.agent_market import AgentMarket
from prediction_market_agent_tooling.markets.data_models import (
ProbabilisticAnswer,
TokenAmountAndDirection,
)
from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
get_kelly_bet,
)


class BettingStrategy(ABC):
@abstractmethod
def calculate_bet_amount_and_direction(
self, answer: ProbabilisticAnswer, market: AgentMarket
) -> TokenAmountAndDirection:
pass


class MaxAccuracyBettingStrategy(BettingStrategy):
def __init__(self, bet_amount: float | None = None):
self.bet_amount = bet_amount

@staticmethod
def calculate_direction(market_p_yes: float, estimate_p_yes: float) -> bool:
# If estimate_p_yes >= market.current_p_yes, then bet TRUE, else bet FALSE.
# This is equivalent to saying EXPECTED_VALUE = (estimate_p_yes * num_tokens_obtained_by_betting_yes) -
# ((1 - estimate_p_yes) * num_tokens_obtained_by_betting_no) >= 0
return estimate_p_yes >= market_p_yes

def calculate_bet_amount_and_direction(
self, answer: ProbabilisticAnswer, market: AgentMarket
) -> TokenAmountAndDirection:
bet_amount = (
market.get_tiny_bet_amount().amount
if self.bet_amount is None
else self.bet_amount
)
direction = self.calculate_direction(market.current_p_yes, answer.p_yes)
return TokenAmountAndDirection(
amount=bet_amount,
currency=market.currency,
direction=direction,
)


class KellyBettingStrategy(BettingStrategy):
def __init__(self, max_bet_amount: float = 10):
self.max_bet_amount = max_bet_amount

def calculate_bet_amount_and_direction(
self, answer: ProbabilisticAnswer, market: AgentMarket
) -> TokenAmountAndDirection:
kelly_bet = get_kelly_bet(
self.max_bet_amount, market.current_p_yes, answer.p_yes, answer.confidence
)
return TokenAmountAndDirection(
amount=kelly_bet.size,
currency=market.currency,
direction=kelly_bet.direction,
)
43 changes: 40 additions & 3 deletions prediction_market_agent_tooling/markets/data_models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from datetime import datetime
from enum import Enum
from typing import TypeAlias
from typing import Annotated, TypeAlias

from pydantic import BaseModel, computed_field
from pydantic import BaseModel, BeforeValidator, computed_field

from prediction_market_agent_tooling.gtypes import OutcomeStr
from prediction_market_agent_tooling.gtypes import OutcomeStr, Probability


class Currency(str, Enum):
Expand Down Expand Up @@ -57,6 +57,43 @@ def __str__(self) -> str:
return f"Resolved bet for market {self.market_id} for question {self.market_question} created at {self.created_time}: {self.amount} on {self.outcome}. Bet was resolved at {self.resolved_time} and was {'correct' if self.is_correct else 'incorrect'}. Profit was {self.profit}"


class TokenAmountAndDirection(TokenAmount):
direction: bool


def to_boolean_outcome(value: str | bool) -> bool:
if isinstance(value, bool):
return value

elif isinstance(value, str):
value = value.lower().strip()

if value in {"true", "yes", "y", "1"}:
return True

elif value in {"false", "no", "n", "0"}:
return False

else:
raise ValueError(f"Expected a boolean string, but got {value}")

else:
raise ValueError(f"Expected a boolean or a string, but got {value}")


Decision = Annotated[bool, BeforeValidator(to_boolean_outcome)]


class ProbabilisticAnswer(BaseModel):
p_yes: Probability
confidence: float
reasoning: str | None = None

@property
def p_no(self) -> Probability:
return Probability(1 - self.p_yes)


class Position(BaseModel):
market_id: str
amounts: dict[OutcomeStr, TokenAmount]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
from enum import Enum

from pydantic import BaseModel


class BetDirection(str, Enum):
YES = "Yes"
NO = "No"
def check_is_valid_probability(probability: float) -> None:
if not 0 <= probability <= 1:
raise ValueError("Probability must be between 0 and 1")


class KellyBet(BaseModel):
direction: BetDirection
direction: bool
size: float


def check_is_valid_probability(probability: float) -> None:
if not 0 <= probability <= 1:
raise ValueError("Probability must be between 0 and 1")


def get_kelly_bet(
max_bet: float,
market_p_yes: float,
Expand Down Expand Up @@ -47,10 +40,10 @@ def get_kelly_bet(
check_is_valid_probability(confidence)

if estimated_p_yes > market_p_yes:
bet_direction = BetDirection.YES
bet_direction = True
market_prob = market_p_yes
else:
bet_direction = BetDirection.NO
bet_direction = False
market_prob = 1 - market_p_yes

# Handle the case where market_prob is 0
Expand Down
5 changes: 1 addition & 4 deletions tests/markets/test_betting_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
WrappedxDaiContract,
)
from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
BetDirection,
get_kelly_bet,
)
from prediction_market_agent_tooling.tools.betting_strategies.market_moving import (
Expand Down Expand Up @@ -204,9 +203,7 @@ def test_kelly_bet(est_p_yes: Probability, omen_market: OmenMarket) -> None:
max_bet = 10
confidence = 1.0
market_p_yes = omen_market.current_p_yes
expected_bet_direction = (
BetDirection.NO if est_p_yes < market_p_yes else BetDirection.YES
)
expected_bet_direction = False if est_p_yes < market_p_yes else True

# Kelly estimates the best bet for maximizing the expected value of the
# logarithm of the wealth. We don't know the real best bet amount, but at
Expand Down
Loading

0 comments on commit 5a57423

Please sign in to comment.