Skip to content

Commit

Permalink
383 move rebalancing logic involving previous positions to strategy (#…
Browse files Browse the repository at this point in the history
…387)

* Implemented calculate_target_position for abstract strategy | Added ToDos for deployableTraderAgent

* Putting everything under calculate_trades

* Cleaning up

* Implemented review comments

* Updated lock

* Fixed pytest

* 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>

* Update prediction_market_agent_tooling/markets/omen/omen.py

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

* Implemented PR comments (2)

* Fixed black

* Removed unnecessary token amount

* Avoiding key error

* Fixing pytest

---------

Co-authored-by: Evan Griffiths <56087052+evangriffiths@users.noreply.github.com>
  • Loading branch information
gabrielfior and evangriffiths authored Sep 9, 2024
1 parent 429e1a8 commit 4caab11
Show file tree
Hide file tree
Showing 6 changed files with 426 additions and 282 deletions.
422 changes: 211 additions & 211 deletions poetry.lock

Large diffs are not rendered by default.

55 changes: 16 additions & 39 deletions prediction_market_agent_tooling/deploy/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from prediction_market_agent_tooling.deploy.betting_strategy import (
BettingStrategy,
MaxAccuracyBettingStrategy,
TradeType,
)
from prediction_market_agent_tooling.deploy.constants import (
MARKET_TYPE_KEY,
Expand All @@ -37,10 +38,8 @@
SortBy,
)
from prediction_market_agent_tooling.markets.data_models import (
BetAmount,
ProbabilisticAnswer,
TokenAmount,
TokenAmountAndDirection,
Trade,
)
from prediction_market_agent_tooling.markets.markets import (
MarketType,
Expand Down Expand Up @@ -110,7 +109,7 @@ class OutOfFundsError(ValueError):

class ProcessedMarket(BaseModel):
answer: ProbabilisticAnswer
amount: BetAmount
trades: list[Trade]


class AnsweredEnum(str, Enum):
Expand Down Expand Up @@ -299,7 +298,6 @@ 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_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 @@ -315,18 +313,6 @@ 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 @@ -442,34 +428,25 @@ def process_market(
self.update_langfuse_trace_by_processed_market(market_type, None)
return None

amount_and_direction = self.calculate_bet_amount_and_direction(answer, market)
existing_position = market.get_position(user_id=APIKeys().bet_from_address)
trades = self.strategy.calculate_trades(existing_position, answer, market)
BettingStrategy.assert_trades_currency_match_markets(market, trades)

if self.place_bet:
logger.info(
f"Placing bet on {market} with direction {amount_and_direction.direction} and amount {amount_and_direction.amount}"
)
for trade in trades:
logger.info(f"Executing trade {trade}")

if not self.allow_opposite_bets:
logger.info(
f"Liquidating existing positions contrary to direction {amount_and_direction.direction}"
)
# If we have an existing position, sell it first if they bet on a different outcome.
market.liquidate_existing_positions(amount_and_direction.direction)

market.place_bet(
amount=TokenAmount(
amount=amount_and_direction.amount,
currency=amount_and_direction.currency,
),
outcome=amount_and_direction.direction,
)
match trade.trade_type:
case TradeType.BUY:
market.buy_tokens(outcome=trade.outcome, amount=trade.amount)
case TradeType.SELL:
market.sell_tokens(outcome=trade.outcome, amount=trade.amount)
case _:
raise ValueError(f"Unexpected trade type {trade.trade_type}.")

self.after_process_market(market_type, market)

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

return processed_market
Expand Down
179 changes: 152 additions & 27 deletions prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,186 @@

from prediction_market_agent_tooling.markets.agent_market import AgentMarket
from prediction_market_agent_tooling.markets.data_models import (
Currency,
Position,
ProbabilisticAnswer,
TokenAmountAndDirection,
TokenAmount,
Trade,
TradeType,
)
from prediction_market_agent_tooling.markets.omen.data_models import get_boolean_outcome
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:
def calculate_trades(
self,
existing_position: Position | None,
answer: ProbabilisticAnswer,
market: AgentMarket,
) -> list[Trade]:
pass

def build_zero_token_amount(self, currency: Currency) -> TokenAmount:
return TokenAmount(amount=0, currency=currency)

class MaxAccuracyBettingStrategy(BettingStrategy):
def __init__(self, bet_amount: float | None = None):
self.bet_amount = bet_amount
@abstractmethod
def adjust_bet_amount(
self, existing_position: Position | None, market: AgentMarket
) -> float:
pass

@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 assert_trades_currency_match_markets(
market: AgentMarket, trades: list[Trade]
) -> None:
currencies_match = all([t.amount.currency == market.currency for t in trades])
if not currencies_match:
raise ValueError(
"Cannot handle trades with currencies that deviate from market's currency"
)

def calculate_bet_amount_and_direction(
self, answer: ProbabilisticAnswer, market: AgentMarket
) -> TokenAmountAndDirection:
def _build_rebalance_trades_from_positions(
self,
existing_position: Position | None,
target_position: Position,
market: AgentMarket,
) -> list[Trade]:
"""
This helper method builds trades by rebalancing token allocations to each outcome.
For example, if we have an existing position with 10 tokens in outcome 0 and 5 in outcome 1,
and our target position is 20 tokens in outcome 0 and 0 in outcome 1, we would return these trades:
trades = [
Trade(outcome=0, amount=10, trade_type=TradeType.BUY),
Trade(outcome=1, amount=5, trade_type=TradeType.SELL)
]
Note that we order the trades to first buy then sell, in order to minimally tilt the odds so that
sell price is higher.
"""
trades = []
for outcome in [
market.get_outcome_str_from_bool(True),
market.get_outcome_str_from_bool(False),
]:
outcome_bool = get_boolean_outcome(outcome)
prev_amount: TokenAmount = (
existing_position.amounts[outcome]
if existing_position and outcome in existing_position.amounts
else self.build_zero_token_amount(currency=market.currency)
)
new_amount: TokenAmount = target_position.amounts.get(
outcome, self.build_zero_token_amount(currency=market.currency)
)

if prev_amount.currency != new_amount.currency:
raise ValueError("Cannot handle positions with different currencies")
diff_amount = prev_amount.amount - new_amount.amount
if diff_amount == 0:
continue
trade_type = TradeType.SELL if diff_amount < 0 else TradeType.BUY
trade = Trade(
amount=TokenAmount(amount=diff_amount, currency=market.currency),
outcome=outcome_bool,
trade_type=trade_type,
)

trades.append(trade)

# Sort inplace with SELL last
trades.sort(key=lambda t: t.trade_type == TradeType.SELL)
BettingStrategy.assert_trades_currency_match_markets(market, trades)
return trades


class MaxAccuracyBettingStrategy(BettingStrategy):
def adjust_bet_amount(
self, existing_position: Position | None, market: AgentMarket
) -> float:
existing_position_total_amount = (
existing_position.total_amount.amount if existing_position else 0
)
bet_amount = (
market.get_tiny_bet_amount().amount
if self.bet_amount is None
else self.bet_amount
)
return bet_amount + existing_position_total_amount

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

def calculate_trades(
self,
existing_position: Position | None,
answer: ProbabilisticAnswer,
market: AgentMarket,
) -> list[Trade]:
adjusted_bet_amount = self.adjust_bet_amount(existing_position, market)

direction = self.calculate_direction(market.current_p_yes, answer.p_yes)
return TokenAmountAndDirection(
amount=bet_amount,
currency=market.currency,
direction=direction,

amounts = {
market.get_outcome_str_from_bool(direction): TokenAmount(
amount=adjusted_bet_amount,
currency=market.currency,
),
}
target_position = Position(market_id=market.id, amounts=amounts)
trades = self._build_rebalance_trades_from_positions(
existing_position, target_position, market=market
)
return trades

@staticmethod
def calculate_direction(market_p_yes: float, estimate_p_yes: float) -> bool:
return estimate_p_yes >= 0.5


class MaxExpectedValueBettingStrategy(MaxAccuracyBettingStrategy):
@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


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:
def adjust_bet_amount(
self, existing_position: Position | None, market: AgentMarket
) -> float:
existing_position_total_amount = (
existing_position.total_amount.amount if existing_position else 0
)
return self.max_bet_amount + existing_position_total_amount

def calculate_trades(
self,
existing_position: Position | None,
answer: ProbabilisticAnswer,
market: AgentMarket,
) -> list[Trade]:
adjusted_bet_amount = self.adjust_bet_amount(existing_position, market)
kelly_bet = get_kelly_bet(
self.max_bet_amount, market.current_p_yes, answer.p_yes, answer.confidence
adjusted_bet_amount,
market.current_p_yes,
answer.p_yes,
answer.confidence,
)
return TokenAmountAndDirection(
amount=kelly_bet.size,
currency=market.currency,
direction=kelly_bet.direction,

amounts = {
market.get_outcome_str_from_bool(kelly_bet.direction): TokenAmount(
amount=kelly_bet.size, currency=market.currency
),
}
target_position = Position(market_id=market.id, amounts=amounts)
trades = self._build_rebalance_trades_from_positions(
existing_position, target_position, market=market
)
return trades
14 changes: 13 additions & 1 deletion prediction_market_agent_tooling/markets/agent_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pydantic_core.core_schema import FieldValidationInfo

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.gtypes import Probability
from prediction_market_agent_tooling.gtypes import OutcomeStr, Probability
from prediction_market_agent_tooling.markets.data_models import (
Bet,
BetAmount,
Expand Down Expand Up @@ -153,6 +153,12 @@ def get_last_trade_no_outcome_price(self) -> float | None:
def get_bet_amount(self, amount: float) -> BetAmount:
return BetAmount(amount=amount, currency=self.currency)

@classmethod
def get_liquidatable_amount(cls) -> BetAmount:
tiny_amount = cls.get_tiny_bet_amount()
tiny_amount.amount /= 10
return tiny_amount

@classmethod
def get_tiny_bet_amount(cls) -> BetAmount:
raise NotImplementedError("Subclasses must implement this method")
Expand Down Expand Up @@ -213,6 +219,9 @@ def has_successful_resolution(self) -> bool:
def has_unsuccessful_resolution(self) -> bool:
return self.resolution in [Resolution.CANCEL, Resolution.MKT]

def get_outcome_str_from_bool(self, outcome: bool) -> OutcomeStr:
raise NotImplementedError("Subclasses must implement this method")

def get_outcome_str(self, outcome_index: int) -> str:
try:
return self.outcomes[outcome_index]
Expand All @@ -230,6 +239,9 @@ def get_outcome_index(self, outcome: str) -> int:
def get_token_balance(self, user_id: str, outcome: str) -> TokenAmount:
raise NotImplementedError("Subclasses must implement this method")

def get_position(self, user_id: str) -> Position | None:
raise NotImplementedError("Subclasses must implement this method")

@classmethod
def get_positions(
cls, user_id: str, liquid_only: bool = False, larger_than: float = 0
Expand Down
11 changes: 11 additions & 0 deletions prediction_market_agent_tooling/markets/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,14 @@ def __str__(self) -> str:
for outcome, amount in self.amounts.items()
)
return f"Position for market id {self.market_id}: {amounts_str}"


class TradeType(str, Enum):
SELL = "sell"
BUY = "buy"


class Trade(BaseModel):
trade_type: TradeType
outcome: bool
amount: TokenAmount
Loading

0 comments on commit 4caab11

Please sign in to comment.