diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index 74a966c8..481bc8b0 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -11,8 +11,10 @@ ) 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, + get_kelly_bet_full, + get_kelly_bet_simplified, ) +from prediction_market_agent_tooling.tools.utils import check_not_none class BettingStrategy(ABC): @@ -168,11 +170,26 @@ def calculate_trades( market: AgentMarket, ) -> list[Trade]: adjusted_bet_amount = self.adjust_bet_amount(existing_position, market) - kelly_bet = get_kelly_bet( - adjusted_bet_amount, - market.current_p_yes, - answer.p_yes, - answer.confidence, + outcome_token_pool = check_not_none(market.outcome_token_pool) + kelly_bet = ( + get_kelly_bet_full( + yes_outcome_pool_size=outcome_token_pool[ + market.get_outcome_str_from_bool(True) + ], + no_outcome_pool_size=outcome_token_pool[ + market.get_outcome_str_from_bool(False) + ], + estimated_p_yes=answer.p_yes, + max_bet=adjusted_bet_amount, + confidence=answer.confidence, + ) + if market.has_token_pool() + else get_kelly_bet_simplified( + adjusted_bet_amount, + market.current_p_yes, + answer.p_yes, + answer.confidence, + ) ) amounts = { diff --git a/prediction_market_agent_tooling/markets/omen/omen.py b/prediction_market_agent_tooling/markets/omen/omen.py index 37643be2..f73a98f3 100644 --- a/prediction_market_agent_tooling/markets/omen/omen.py +++ b/prediction_market_agent_tooling/markets/omen/omen.py @@ -577,6 +577,49 @@ def get_positions_value(cls, positions: list[Position]) -> BetAmount: def get_user_url(cls, keys: APIKeys) -> str: return get_omen_user_url(keys.bet_from_address) + def get_buy_token_amount( + self, bet_amount: BetAmount, direction: bool + ) -> TokenAmount: + received_token_amount_wei = Wei( + self.get_contract().calcBuyAmount( + investment_amount=xdai_to_wei(xDai(bet_amount.amount)), + outcome_index=self.get_outcome_index( + self.get_outcome_str_from_bool(direction) + ), + ) + ) + received_token_amount = float(wei_to_xdai(received_token_amount_wei)) + return TokenAmount(amount=received_token_amount, currency=self.currency) + + def get_new_p_yes(self, bet_amount: BetAmount, direction: bool) -> Probability: + """ + Calculate the new p_yes based on the bet amount and direction. + """ + if not self.has_token_pool(): + raise ValueError("Outcome token pool is required to calculate new p_yes.") + + outcome_token_pool = check_not_none(self.outcome_token_pool) + yes_outcome_pool_size = outcome_token_pool[self.get_outcome_str_from_bool(True)] + no_outcome_pool_size = outcome_token_pool[self.get_outcome_str_from_bool(False)] + + new_yes_outcome_pool_size = yes_outcome_pool_size + ( + bet_amount.amount * (1 - self.fee) + ) + new_no_outcome_pool_size = no_outcome_pool_size + ( + bet_amount.amount * (1 - self.fee) + ) + + received_token_amount = self.get_buy_token_amount(bet_amount, direction).amount + if direction: + new_yes_outcome_pool_size -= received_token_amount + else: + new_no_outcome_pool_size -= received_token_amount + + new_p_yes = new_no_outcome_pool_size / ( + new_yes_outcome_pool_size + new_no_outcome_pool_size + ) + return Probability(new_p_yes) + def get_omen_user_url(address: ChecksumAddress) -> str: return f"https://gnosisscan.io/address/{address}" diff --git a/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py b/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py index dc74fb12..a7d36f0b 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet def check_is_valid_probability(probability: float) -> None: @@ -6,17 +6,12 @@ def check_is_valid_probability(probability: float) -> None: raise ValueError("Probability must be between 0 and 1") -class KellyBet(BaseModel): - direction: bool - size: float - - -def get_kelly_bet( +def get_kelly_bet_simplified( max_bet: float, market_p_yes: float, estimated_p_yes: float, confidence: float, -) -> KellyBet: +) -> SimpleBet: """ Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market. @@ -57,4 +52,86 @@ def get_kelly_bet( # Ensure bet size is non-negative does not exceed the wallet balance bet_size = min(kelly_fraction * max_bet, max_bet) - return KellyBet(direction=bet_direction, size=bet_size) + return SimpleBet(direction=bet_direction, size=bet_size) + + +def get_kelly_bet_full( + yes_outcome_pool_size: float, + no_outcome_pool_size: float, + estimated_p_yes: float, + confidence: float, + max_bet: float, + fee: float = 0.0, # proportion, 0 to 1 +) -> SimpleBet: + """ + Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market. + + 'Full' as in it accounts for how the bet changes the market odds. + + Taken from https://github.com/valory-xyz/trader/blob/main/strategies/kelly_criterion/kelly_criterion.py + + with derivation in PR description: https://github.com/valory-xyz/trader/pull/119 + + ``` + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ``` + """ + check_is_valid_probability(estimated_p_yes) + check_is_valid_probability(confidence) + check_is_valid_probability(fee) + + if max_bet == 0: + return SimpleBet(direction=True, size=0) + + x = yes_outcome_pool_size + y = no_outcome_pool_size + p = estimated_p_yes + c = confidence + b = max_bet + f = 1 - fee + + numerator = ( + -4 * x**2 * y + + b * y**2 * p * c * f + + 2 * b * x * y * p * c * f + + b * x**2 * p * c * f + - 2 * b * y**2 * f + - 2 * b * x * y * f + + ( + ( + 4 * x**2 * y + - b * y**2 * p * c * f + - 2 * b * x * y * p * c * f + - b * x**2 * p * c * f + + 2 * b * y**2 * f + + 2 * b * x * y * f + ) + ** 2 + - ( + 4 + * (x**2 * f - y**2 * f) + * ( + -4 * b * x * y**2 * p * c + - 4 * b * x**2 * y * p * c + + 4 * b * x * y**2 + ) + ) + ) + ** (1 / 2) + ) + denominator = 2 * (x**2 * f - y**2 * f) + if denominator == 0: + return SimpleBet(direction=True, size=0) + kelly_bet_amount = numerator / denominator + + return SimpleBet(direction=kelly_bet_amount > 0, size=abs(kelly_bet_amount)) diff --git a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py index 1823b5fb..32446ff2 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -1,29 +1,22 @@ -import typing as t from functools import reduce import numpy as np -from prediction_market_agent_tooling.gtypes import Probability, wei_type, xDai -from prediction_market_agent_tooling.loggers import logger -from prediction_market_agent_tooling.markets.omen.data_models import OmenMarket +from prediction_market_agent_tooling.gtypes import Probability, Wei, xDai from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket +from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet from prediction_market_agent_tooling.tools.utils import check_not_none -from prediction_market_agent_tooling.tools.web3_utils import ( - ONE_XDAI, - wei_to_xdai, - xdai_to_wei, -) - -OutcomeIndex = t.Literal[0, 1] +from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai, xdai_to_wei def get_market_moving_bet( - market: OmenMarket, - target_p_yes: Probability, + yes_outcome_pool_size: float, + no_outcome_pool_size: float, + market_p_yes: float, + target_p_yes: float, + fee: float = 0.0, # proportion, 0 to 1 max_iters: int = 100, - check_vs_contract: bool = False, # Disable by default, as it's slow - verbose: bool = False, -) -> t.Tuple[xDai, OutcomeIndex]: +) -> SimpleBet: """ Implements a binary search to determine the bet that will move the market's `p_yes` to that of the target. @@ -43,74 +36,97 @@ def get_market_moving_bet( new_product - fixed_product = dx * na_y dx = (new_product - fixed_product) / na_y """ - market_agent = OmenAgentMarket.from_data_model(market) - amounts = market.outcomeTokenAmounts - prices = check_not_none( - market.outcomeTokenProbabilities, "No probabilities, is marked closed?" - ) - if len(amounts) != 2 or len(prices) != 2: - raise ValueError("Only binary markets are supported.") - - fixed_product = reduce(lambda x, y: x * y, amounts, 1) - assert np.isclose(float(sum(prices)), 1) + fixed_product = yes_outcome_pool_size * no_outcome_pool_size + bet_direction: bool = target_p_yes > market_p_yes - # For FPMMs, the probability is equal to the marginal price - current_p_yes = Probability(prices[0]) - bet_outcome_index: OutcomeIndex = 0 if target_p_yes > current_p_yes else 1 - - min_bet_amount = 0 - max_bet_amount = 100 * sum(amounts) # TODO set a better upper bound + min_bet_amount = 0.0 + max_bet_amount = 100 * ( + yes_outcome_pool_size + no_outcome_pool_size + ) # TODO set a better upper bound # Binary search for the optimal bet amount for _ in range(max_iters): - bet_amount = (min_bet_amount + max_bet_amount) // 2 - bet_amount_ = ( - bet_amount - * ( - xdai_to_wei(ONE_XDAI) - - check_not_none(market.fee, "No fee for the market.") - ) - / xdai_to_wei(ONE_XDAI) - ) + bet_amount = (min_bet_amount + max_bet_amount) / 2 + amounts_diff = bet_amount * (1 - fee) # Initial new amounts are old amounts + equal new amounts for each outcome - amounts_diff = bet_amount_ - new_amounts = [amounts[i] + amounts_diff for i in range(len(amounts))] + yes_outcome_new_pool_size = yes_outcome_pool_size + amounts_diff + no_outcome_new_pool_size = no_outcome_pool_size + amounts_diff + new_amounts = { + True: yes_outcome_new_pool_size, + False: no_outcome_new_pool_size, + } # Now give away tokens at `bet_outcome_index` to restore invariant - new_product = reduce(lambda x, y: x * y, new_amounts, 1.0) - dx = (new_product - fixed_product) / new_amounts[1 - bet_outcome_index] - - # Sanity check the number of tokens against the contract - if check_vs_contract: - expected_trade = market_agent.get_contract().calcBuyAmount( - investment_amount=wei_type(bet_amount), - outcome_index=bet_outcome_index, - ) - assert np.isclose(float(expected_trade), dx) - - new_amounts[bet_outcome_index] -= dx + new_product = yes_outcome_new_pool_size * no_outcome_new_pool_size + dx = (new_product - fixed_product) / new_amounts[not bet_direction] + new_amounts[bet_direction] -= dx + # Check that the invariant is restored assert np.isclose( - reduce(lambda x, y: x * y, new_amounts, 1.0), float(fixed_product) + reduce(lambda x, y: x * y, list(new_amounts.values()), 1.0), + float(fixed_product), ) - new_p_yes = Probability(new_amounts[1] / sum(new_amounts)) - bet_amount_wei = wei_type(bet_amount) - if verbose: - outcome = market_agent.get_outcome_str(bet_outcome_index) - logger.debug( - f"Target p_yes: {target_p_yes:.2f}, bet: {wei_to_xdai(bet_amount_wei):.2f}{market_agent.currency} for {outcome}, new p_yes: {new_p_yes:.2f}" - ) - if abs(target_p_yes - new_p_yes) < 0.01: + + new_p_yes = Probability(new_amounts[False] / sum(list(new_amounts.values()))) + if abs(target_p_yes - new_p_yes) < 1e-6: break elif new_p_yes > target_p_yes: - if bet_outcome_index == 0: + if bet_direction: max_bet_amount = bet_amount else: min_bet_amount = bet_amount else: - if bet_outcome_index == 0: + if bet_direction: min_bet_amount = bet_amount else: max_bet_amount = bet_amount - return wei_to_xdai(bet_amount_wei), bet_outcome_index + + return SimpleBet(direction=bet_direction, size=bet_amount) + + +def _sanity_check_omen_market_moving_bet( + bet_to_check: SimpleBet, market: OmenAgentMarket, target_p_yes: float +) -> None: + """ + A util function for checking that a bet moves the market to the target_p_yes + by calling the market's calcBuyAmount method from the smart contract, and + using the adjusted outcome pool sizes to calculate the new p_yes. + """ + buy_amount_ = market.get_contract().calcBuyAmount( + investment_amount=xdai_to_wei(xDai(bet_to_check.size)), + outcome_index=market.get_outcome_index( + market.get_outcome_str_from_bool(bet_to_check.direction) + ), + ) + buy_amount = float(wei_to_xdai(Wei(buy_amount_))) + + outcome_token_pool = check_not_none(market.outcome_token_pool) + yes_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(True)] + no_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(False)] + market_const = yes_outcome_pool_size * no_outcome_pool_size + + # When you buy 'yes' tokens, you add your bet size to the both pools, then + # subtract `buy_amount` from the 'yes' pool. And vice versa for 'no' tokens. + new_yes_outcome_pool_size = ( + yes_outcome_pool_size + + (bet_to_check.size * (1 - market.fee)) + - float(bet_to_check.direction) * buy_amount + ) + new_no_outcome_pool_size = ( + no_outcome_pool_size + + (bet_to_check.size * (1 - market.fee)) + - float(not bet_to_check.direction) * buy_amount + ) + new_market_const = new_yes_outcome_pool_size * new_no_outcome_pool_size + # Check the invariant is restored + assert np.isclose(new_market_const, market_const) + + # Now check that the market's new p_yes is equal to the target_p_yes + new_p_yes = new_no_outcome_pool_size / ( + new_yes_outcome_pool_size + new_no_outcome_pool_size + ) + if not np.isclose(new_p_yes, target_p_yes, atol=0.01): + raise ValueError( + f"Bet does not move market to target_p_yes {target_p_yes=}. Got {new_p_yes=}" + ) diff --git a/prediction_market_agent_tooling/tools/betting_strategies/utils.py b/prediction_market_agent_tooling/tools/betting_strategies/utils.py new file mode 100644 index 00000000..354282d6 --- /dev/null +++ b/prediction_market_agent_tooling/tools/betting_strategies/utils.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class SimpleBet(BaseModel): + direction: bool + size: float diff --git a/scripts/mm_vs_kelly_plot.py b/scripts/mm_vs_kelly_plot.py new file mode 100644 index 00000000..86fa9308 --- /dev/null +++ b/scripts/mm_vs_kelly_plot.py @@ -0,0 +1,111 @@ +import numpy as np +import pytest +from matplotlib import pyplot as plt + +from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy +from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket +from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import ( + get_kelly_bet_full, +) +from prediction_market_agent_tooling.tools.betting_strategies.market_moving import ( + _sanity_check_omen_market_moving_bet, + get_market_moving_bet, +) + +market = OmenAgentMarket.get_binary_markets( + limit=1, + sort_by=SortBy.CLOSING_SOONEST, + filter_by=FilterBy.OPEN, +)[0] + +yes_outcome_pool_size = market.outcome_token_pool[ + market.get_outcome_str_from_bool(True) +] +no_outcome_pool_size = market.outcome_token_pool[ + market.get_outcome_str_from_bool(False) +] +max_bets = np.linspace(0.0, 200, 100) + +# Define a list of colors for different estimated_p_yes values +colors = plt.cm.viridis(np.linspace(0, 1, len([0.05, 0.42, 0.71]))) + +for i, estimated_p_yes in enumerate([0.57, 0.74, 0.92]): + kelly_bets = [] + market_moving_bets = [] + kelly_expected_values = [] + market_moving_bet = get_market_moving_bet( + yes_outcome_pool_size=yes_outcome_pool_size, + no_outcome_pool_size=no_outcome_pool_size, + market_p_yes=market.current_p_yes, + target_p_yes=estimated_p_yes, + fee=market.fee, + ) + _sanity_check_omen_market_moving_bet(market_moving_bet, market, estimated_p_yes) + + market_moving_bet_signed = ( + market_moving_bet.size + if market_moving_bet.direction == True + else -market_moving_bet.size + ) + + for max_bet in max_bets: + kelly_bet = get_kelly_bet_full( + yes_outcome_pool_size=yes_outcome_pool_size, + no_outcome_pool_size=no_outcome_pool_size, + estimated_p_yes=estimated_p_yes, + confidence=1.0, + max_bet=max_bet, + fee=market.fee, + ) + kelly_bet_signed = ( + kelly_bet.size if kelly_bet.direction == True else -kelly_bet.size + ) + kelly_bets.append(kelly_bet_signed) + market_moving_bets.append(market_moving_bet_signed) + + # Calculate expected value for kelly bet + p_correct = estimated_p_yes if kelly_bet.direction else 1 - estimated_p_yes + tokens_bought = market.get_buy_token_amount( + market.get_bet_amount(kelly_bet.size), kelly_bet.direction + ) + expected_value = (tokens_bought.amount * p_correct) - ( + kelly_bet.size * (1 - p_correct) + ) + kelly_expected_values.append(expected_value) + + # Kelly bet does not converge to market-moving bet. Is that expected? Yes! + if max_bet == max_bets[-1]: + with pytest.raises(ValueError) as e: + _sanity_check_omen_market_moving_bet(kelly_bet, market, estimated_p_yes) + assert e.match("Bet does not move market to target_p_yes") + + plt.plot( + max_bets, + kelly_bets, + label=f"Kelly bet, {estimated_p_yes=:.2f}", + color=colors[i], + linestyle="-", + ) + plt.plot( + max_bets, + market_moving_bets, + label=f"Market-moving bet, target_p_yes={estimated_p_yes:.2f}", + color=colors[i], + linestyle="--", + ) + plt.plot( + max_bets, + kelly_expected_values, + label=f"Kelly bet EV, target_p_yes={estimated_p_yes:.2f}", + color=colors[i], + linestyle="-.", + ) + +plt.xlabel(f"Max bet (xDai)") +plt.ylabel("Kelly bet size (xDai)") +plt.title( + f"Market-moving vs. kelly bet, market_p_yes={market.current_p_yes:.2f}, pool_size={(yes_outcome_pool_size+no_outcome_pool_size):.2f}" +) +plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") +plt.tight_layout() +plt.show() diff --git a/scripts/sequence_of_kelly_bets_convergence.py b/scripts/sequence_of_kelly_bets_convergence.py new file mode 100644 index 00000000..06ae47c5 --- /dev/null +++ b/scripts/sequence_of_kelly_bets_convergence.py @@ -0,0 +1,114 @@ +import numpy as np +from matplotlib import pyplot as plt + +from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import ( + get_kelly_bet_full, +) +from prediction_market_agent_tooling.tools.betting_strategies.market_moving import ( + get_market_moving_bet, +) + +""" +if kelly moves market past mm bet, does subsequent market move back to mm bet? +""" + + +def get_p_yes_from_token_pool(token_pool: dict[bool, float]) -> float: + return token_pool[False] / (token_pool[True] + token_pool[False]) + + +def get_buy_amount( + token_pool: dict[bool, float], bet_amount: float, direction: bool +) -> float: + """ + y: yes outcome pool size + n: no outcome pool size + b: bet amount + d: float(direction) + d': float(not direction) + c: market constant + a: buy amount + + Initially: + y * n = c + + After bet: + (y + b - a.d).(n + b - a.d') = c + + Rearrange to solve for a: + a = ((b + y)(b + n) - c) / (y.d' + n.d + b) + """ + y = token_pool[True] + n = token_pool[False] + b = bet_amount + c = y * n + d = float(direction) + d_prime = float(not direction) + a = ((b + y) * (b + n) - c) / (y * d_prime + n * d + b) + return a + + +def get_new_token_pool_after_bet( + orig_token_pool: dict[bool, float], bet_amount: float, direction: bool +) -> dict[bool, float]: + new_token_pool = orig_token_pool.copy() + for outcome in [True, False]: + new_token_pool[outcome] += bet_amount + buy_amount = get_buy_amount(orig_token_pool, bet_amount, direction) + new_token_pool[direction] -= buy_amount + return new_token_pool + + +estimated_p_yes = 0.90 +kelly_max_bet_mm_bet_ratios = [0.1, 0.2, 0.5, 1.0, 5, 10, 100, 1000, 10000] +colors = plt.cm.viridis(np.linspace(0, 1, len(kelly_max_bet_mm_bet_ratios))) +n_sequential_bets = 20 + +# Init token pool +outcome_token_pool = { + True: 10.0, + False: 11.0, +} + +market_moving_bet = get_market_moving_bet( + yes_outcome_pool_size=outcome_token_pool[True], + no_outcome_pool_size=outcome_token_pool[False], + market_p_yes=get_p_yes_from_token_pool(outcome_token_pool), + target_p_yes=estimated_p_yes, +) + +for i, bet_ratio in enumerate(kelly_max_bet_mm_bet_ratios): + # Add initial p_yes + p_yess = [get_p_yes_from_token_pool(outcome_token_pool)] + updated_outcome_token_pool = outcome_token_pool.copy() + + for _ in range(n_sequential_bets): + kelly_bet = get_kelly_bet_full( + yes_outcome_pool_size=updated_outcome_token_pool[True], + no_outcome_pool_size=updated_outcome_token_pool[False], + estimated_p_yes=estimated_p_yes, + confidence=1.0, + max_bet=market_moving_bet.size * bet_ratio, + ) + + # Update token pool + updated_outcome_token_pool = get_new_token_pool_after_bet( + updated_outcome_token_pool, kelly_bet.size, kelly_bet.direction + ) + p_yess.append(get_p_yes_from_token_pool(updated_outcome_token_pool)) + + plt.plot( + range(len(p_yess)), + p_yess, + label=f"kelly 'max bet':market-moving bet ratio = {bet_ratio}", + color=colors[i], + linestyle="-", + ) + + +plt.xlabel(f"Bet number") +plt.ylabel("Market p_yes") +plt.title("Kelly bet convergence to estimated_p_yes") +plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") +plt.tight_layout() +plt.show() diff --git a/tests/markets/omen/test_omen.py b/tests/markets/omen/test_omen.py index 16360ccf..c6916e54 100644 --- a/tests/markets/omen/test_omen.py +++ b/tests/markets/omen/test_omen.py @@ -17,6 +17,9 @@ from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import ( OmenSubgraphHandler, ) +from prediction_market_agent_tooling.tools.betting_strategies.market_moving import ( + get_market_moving_bet, +) from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai @@ -207,3 +210,36 @@ def bet_to_position(bet: OmenBet) -> Position: rtol=1e-3, # relax tolerances due to fees atol=1e-3, ) + + +def test_get_new_p_yes() -> None: + market = OmenAgentMarket.get_binary_markets( + limit=1, + sort_by=SortBy.CLOSING_SOONEST, + filter_by=FilterBy.OPEN, + )[0] + assert ( + market.get_new_p_yes(bet_amount=market.get_bet_amount(10.0), direction=True) + > market.current_p_yes + ) + assert ( + market.get_new_p_yes(bet_amount=market.get_bet_amount(11.0), direction=False) + < market.current_p_yes + ) + + # Sanity check vs market moving bet + target_p_yes = 0.95 + outcome_token_pool = check_not_none(market.outcome_token_pool) + yes_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(True)] + no_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(False)] + bet = get_market_moving_bet( + yes_outcome_pool_size=yes_outcome_pool_size, + no_outcome_pool_size=no_outcome_pool_size, + market_p_yes=market.current_p_yes, + target_p_yes=0.95, + fee=market.fee, + ) + new_p_yes = market.get_new_p_yes( + bet_amount=market.get_bet_amount(bet.size), direction=bet.direction + ) + assert np.isclose(new_p_yes, target_p_yes) diff --git a/tests/markets/test_betting_strategies.py b/tests/markets/test_betting_strategies.py index 18585093..7e22d9ca 100644 --- a/tests/markets/test_betting_strategies.py +++ b/tests/markets/test_betting_strategies.py @@ -14,9 +14,9 @@ mana_type, usd_type, wei_type, - xDai, xdai_type, ) +from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy from prediction_market_agent_tooling.markets.manifold.manifold import ( ManifoldAgentMarket, ) @@ -30,9 +30,11 @@ WrappedxDaiContract, ) from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import ( - get_kelly_bet, + get_kelly_bet_full, + get_kelly_bet_simplified, ) from prediction_market_agent_tooling.tools.betting_strategies.market_moving import ( + _sanity_check_omen_market_moving_bet, get_market_moving_bet, ) from prediction_market_agent_tooling.tools.betting_strategies.minimum_bet_to_win import ( @@ -41,7 +43,8 @@ from prediction_market_agent_tooling.tools.betting_strategies.stretch_bet_between import ( stretch_bet_between, ) -from prediction_market_agent_tooling.tools.utils import utcnow +from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow +from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai GANACHE_ADDRESS_NR_1 = HexAddress( Web3.to_checksum_address("0x9B7bc47837d4061a11389267C06D829c5C97E404") @@ -158,31 +161,57 @@ def test_minimum_bet_to_win_manifold( @pytest.mark.parametrize( - "wanted_p_yes_on_the_market, expected_buying_xdai_amount, expected_buying_outcome", + "target_p_yes, expected_bet_size, expected_bet_direction", [ - (Probability(0.1), xdai_type(25.32), "No"), - (Probability(0.9), xdai_type(18.1), "Yes"), + (Probability(0.1), xdai_type(23.19), False), + (Probability(0.9), xdai_type(18.1), True), ], ) def test_get_market_moving_bet( - wanted_p_yes_on_the_market: Probability, - expected_buying_xdai_amount: xDai, - expected_buying_outcome: str, + target_p_yes: Probability, + expected_bet_size: float, + expected_bet_direction: bool, omen_market: OmenMarket, ) -> None: - xdai_amount, outcome_index = get_market_moving_bet( - market=omen_market, - target_p_yes=wanted_p_yes_on_the_market, - verbose=True, + bet = get_market_moving_bet( + target_p_yes=target_p_yes, + market_p_yes=omen_market.current_p_yes, + yes_outcome_pool_size=wei_to_xdai( + Wei(omen_market.outcomeTokenAmounts[omen_market.yes_index]) + ), + no_outcome_pool_size=wei_to_xdai( + Wei(omen_market.outcomeTokenAmounts[omen_market.no_index]) + ), + fee=wei_to_xdai(check_not_none(omen_market.fee)), ) assert np.isclose( - float(xdai_amount), - float(expected_buying_xdai_amount), + bet.size, + expected_bet_size, atol=2.0, # We don't expect it to be 100% accurate, but close enough. - ), f"To move this martket to ~{wanted_p_yes_on_the_market}% for yes, the amount should be {expected_buying_xdai_amount}xDai, according to aiomen website." - assert outcome_index == omen_market.outcomes.index( - expected_buying_outcome - ), f"The buying outcome index should `{expected_buying_outcome}`." + ) + assert bet.direction == expected_bet_direction + + +@pytest.mark.parametrize("target_p_yes", [0.1, 0.51, 0.9]) +def test_sanity_check_market_moving_bet(target_p_yes: float) -> None: + market = OmenAgentMarket.get_binary_markets( + limit=1, + sort_by=SortBy.CLOSING_SOONEST, + filter_by=FilterBy.OPEN, + )[0] + + outcome_token_pool = check_not_none(market.outcome_token_pool) + yes_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(True)] + no_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(False)] + + market_moving_bet = get_market_moving_bet( + yes_outcome_pool_size=yes_outcome_pool_size, + no_outcome_pool_size=no_outcome_pool_size, + market_p_yes=market.current_p_yes, + target_p_yes=target_p_yes, + fee=market.fee, + ) + _sanity_check_omen_market_moving_bet(market_moving_bet, market, target_p_yes) @pytest.mark.parametrize( @@ -211,7 +240,7 @@ def test_kelly_bet(est_p_yes: Probability, omen_market: OmenMarket) -> None: # logarithm of the wealth. We don't know the real best bet amount, but at # least we know which bet direction makes sense. assert ( - get_kelly_bet( + get_kelly_bet_simplified( market_p_yes=omen_market.current_p_yes, estimated_p_yes=est_p_yes, max_bet=max_bet, @@ -219,3 +248,57 @@ def test_kelly_bet(est_p_yes: Probability, omen_market: OmenMarket) -> None: ).direction == expected_bet_direction ) + + assert ( + get_kelly_bet_full( + yes_outcome_pool_size=wei_to_xdai( + Wei(omen_market.outcomeTokenAmounts[omen_market.yes_index]) + ), + no_outcome_pool_size=wei_to_xdai( + Wei(omen_market.outcomeTokenAmounts[omen_market.no_index]) + ), + estimated_p_yes=est_p_yes, + max_bet=max_bet, + confidence=confidence, + ).direction + == expected_bet_direction + ) + + +def test_zero_bets() -> None: + market = OmenAgentMarket.get_binary_markets( + limit=1, + sort_by=SortBy.CLOSING_SOONEST, + filter_by=FilterBy.OPEN, + )[0] + + outcome_token_pool = check_not_none(market.outcome_token_pool) + yes_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(True)] + no_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(False)] + + market_moving_bet = get_market_moving_bet( + yes_outcome_pool_size=yes_outcome_pool_size, + no_outcome_pool_size=no_outcome_pool_size, + market_p_yes=market.current_p_yes, + target_p_yes=market.current_p_yes, + fee=market.fee, + ) + assert np.isclose(market_moving_bet.size, 0.0, atol=1e-4) + + kelly_bet = get_kelly_bet_full( + yes_outcome_pool_size=yes_outcome_pool_size, + no_outcome_pool_size=no_outcome_pool_size, + estimated_p_yes=market.current_p_yes, + confidence=1.0, + max_bet=0, + fee=market.fee, + ) + assert kelly_bet.size == 0 + + kelly_bet_simple = get_kelly_bet_simplified( + max_bet=100, + market_p_yes=market.current_p_yes, + estimated_p_yes=market.current_p_yes, + confidence=1.0, + ) + assert kelly_bet_simple.size == 0