Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor DeployableTraderAgent to 1. predict a probability for a market then 2. place a bet based on its BettingStrategy #352

Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7f6c0a7
Refactored tiny_bet_amount to strategy
gabrielfior Aug 19, 2024
626c42d
Updating deps
gabrielfior Aug 20, 2024
ed21ffd
Refactoring betting strategies
gabrielfior Aug 20, 2024
faf35e2
Added default strategies
gabrielfior Aug 20, 2024
a4fc8c9
Refactoring betting strategies
gabrielfior Aug 20, 2024
d51301e
Merge remote-tracking branch 'refs/remotes/origin/main' into 338-chan…
gabrielfior Aug 20, 2024
bcbe375
Added lock
gabrielfior Aug 20, 2024
7bbb0c5
Bump version
gabrielfior Aug 20, 2024
74da0d1
Delete run_all.sh
gabrielfior Aug 20, 2024
01ba8f2
Fixing tests
gabrielfior Aug 20, 2024
d6f4b14
Updated GNOSIS_RPC
gabrielfior Aug 20, 2024
1ae480a
Added exception message pattern to be retried
gabrielfior Aug 20, 2024
4515bdd
Refactored exception message
gabrielfior Aug 20, 2024
f073fb8
Added ape as dependency again
gabrielfior Aug 20, 2024
14c9250
Implementing PR comments (1)
gabrielfior Aug 21, 2024
0c8bf96
Update prediction_market_agent_tooling/deploy/agent.py
gabrielfior Aug 21, 2024
9e85c79
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Aug 21, 2024
4432d3b
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Aug 21, 2024
683b95f
Refactored get_tiny_bet_amount
gabrielfior Aug 21, 2024
c013ee2
Refactored Answer into ProbabilisticAnswer, since direction removed
gabrielfior Aug 21, 2024
080ec6e
Updated lock
gabrielfior Aug 21, 2024
cc25098
Merge remote-tracking branch 'refs/remotes/origin/main' into 338-chan…
gabrielfior Aug 21, 2024
99d1eee
Small changes after merge
gabrielfior Aug 21, 2024
0fcd66d
Added calculator for directions in betting strategies
gabrielfior Aug 21, 2024
fc97df9
Removed Answer
gabrielfior Aug 21, 2024
8dce9ce
Added comment to calculator classes | renaming for clarity
gabrielfior Aug 21, 2024
82bd7b9
Removed unnecessary share calculations, replacing that with clever math
gabrielfior Aug 23, 2024
913be0c
Merge remote-tracking branch 'refs/remotes/origin/main' into 338-chan…
gabrielfior Aug 23, 2024
a033c5e
Updated lock file
gabrielfior Aug 23, 2024
932cfa2
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Aug 23, 2024
6e66394
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Aug 23, 2024
742db1d
Implemented PR changes
gabrielfior Aug 23, 2024
c2cf786
Merge remote-tracking branch 'origin/338-change-default-betting-strat…
gabrielfior Aug 23, 2024
0b886fb
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Aug 23, 2024
45f9545
Fixing CI
gabrielfior Aug 23, 2024
0e9386a
Added configurable bet_amount
gabrielfior Aug 23, 2024
9eb4140
Implemented PR changes
gabrielfior Aug 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 88 additions & 89 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
59 changes: 38 additions & 21 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 @@ -296,6 +295,7 @@ def initialize_langfuse(self) -> None:
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 +311,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,13 +372,15 @@ 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:
def calculate_bet_amount(
gabrielfior marked this conversation as resolved.
Show resolved Hide resolved
self, answer: ProbabilisticAnswer, market: AgentMarket
) -> BetAmount:
"""
Calculate the bet amount. By default, it returns the minimum bet amount.
"""
Expand Down Expand Up @@ -408,22 +422,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!")
58 changes: 58 additions & 0 deletions prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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):
@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
gabrielfior marked this conversation as resolved.
Show resolved Hide resolved
direction = self.calculate_direction(market.current_p_yes, answer.p_yes)
return TokenAmountAndDirection(
amount=bet_amount,
currency=market.currency,
direction=direction,
)


class KellyBettingStrategy(BettingStrategy):
@staticmethod
def get_max_bet_amount_for_market() -> float:
gabrielfior marked this conversation as resolved.
Show resolved Hide resolved
# No difference between markets.
return 10
evangriffiths marked this conversation as resolved.
Show resolved Hide resolved

def calculate_bet_amount_and_direction(
self, answer: ProbabilisticAnswer, market: AgentMarket
) -> TokenAmountAndDirection:
max_bet_amount = self.get_max_bet_amount_for_market()
kelly_bet = get_kelly_bet(
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this change from a YES enum to a boolean required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was discussed in another thread (can't remember which one from the 80 comments) - if you have specific concerns, happy to open a new PR to discuss having "YES" or True.

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: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ sqlmodel = "^0.0.21"
psycopg2-binary = "^2.9.9"
base58 = ">=1.0.2,<2.0"


[tool.poetry.extras]
openai = ["openai"]
langchain = ["langchain", "langchain-openai"]
Expand All @@ -57,8 +58,8 @@ google = ["google-api-python-client"]
pytest = "*"
mypy = "^1.11.1"
black = "^23.12.1"
ape-foundry = "^0.8.2"
eth-ape = "^0.8.10"
eth-ape = "^0.8.12"
ape-foundry = "^0.8.4"
gabrielfior marked this conversation as resolved.
Show resolved Hide resolved

[build-system]
requires = ["poetry-core"]
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
Loading