From 9bbf51f5a7cbd6b18d323b12278020c63c83220f Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Mon, 9 Sep 2024 16:59:49 +0100 Subject: [PATCH 01/14] Integrate AgentMarket.outcome_token_pool into betting strategies --- .../deploy/betting_strategy.py | 27 ++++- .../betting_strategies/kelly_criterion.py | 96 ++++++++++++++++-- .../tools/betting_strategies/market_moving.py | 98 ++++++------------- .../tools/betting_strategies/utils.py | 6 ++ tests/markets/test_betting_strategies.py | 60 ++++++++---- 5 files changed, 189 insertions(+), 98 deletions(-) create mode 100644 prediction_market_agent_tooling/tools/betting_strategies/utils.py diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index d0e79cbf..dc433f72 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -5,9 +5,12 @@ ProbabilisticAnswer, TokenAmountAndDirection, ) +from prediction_market_agent_tooling.markets.omen.data_models import OMEN_TRUE_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): @@ -52,8 +55,26 @@ def __init__(self, max_bet_amount: float = 10): 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 + # TODO use market.get_outcome_str_from_bool after https://github.com/gnosis/prediction-market-agent-tooling/pull/387 merges + kelly_bet = ( + get_kelly_bet_full( + yes_outcome_pool_size=check_not_none(market.outcome_token_pool)[ + OMEN_TRUE_OUTCOME + ], + no_outcome_pool_size=check_not_none(market.outcome_token_pool)[ + OMEN_TRUE_OUTCOME + ], + estimated_p_yes=answer.p_yes, + max_bet=self.max_bet_amount, + confidence=answer.confidence, + ) + if market.has_token_pool() + else get_kelly_bet_simplified( + self.max_bet_amount, + market.current_p_yes, + answer.p_yes, + answer.confidence, + ) ) return TokenAmountAndDirection( amount=kelly_bet.size, 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..3323a276 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,87 @@ 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: + breakpoint() + 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..8e5d938a 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,19 @@ -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.markets.omen.omen import OmenAgentMarket -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.gtypes import Probability +from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet 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 +33,50 @@ 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 = True if target_p_yes > market_p_yes else False - # 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) - ) + 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] + 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 - # 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 # 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}" - ) + + new_p_yes = Probability(new_amounts[False] / sum(new_amounts)) if abs(target_p_yes - new_p_yes) < 0.01: 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) 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/tests/markets/test_betting_strategies.py b/tests/markets/test_betting_strategies.py index 18585093..7fe1a118 100644 --- a/tests/markets/test_betting_strategies.py +++ b/tests/markets/test_betting_strategies.py @@ -14,7 +14,6 @@ mana_type, usd_type, wei_type, - xDai, xdai_type, ) from prediction_market_agent_tooling.markets.manifold.manifold import ( @@ -30,7 +29,8 @@ 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 ( get_market_moving_bet, @@ -41,7 +41,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 +159,35 @@ 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(25.32), 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( @@ -211,7 +216,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 +224,18 @@ 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 + ) From 29ab1cf6003deca5e65af31205ff93874be407fd Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Mon, 9 Sep 2024 21:06:08 +0100 Subject: [PATCH 02/14] fix --- .../tools/betting_strategies/market_moving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8e5d938a..92b104a8 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -43,7 +43,7 @@ def get_market_moving_bet( # Binary search for the optimal bet amount for _ in range(max_iters): - bet_amount = (min_bet_amount + max_bet_amount) // 2 + 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 From 68a435601baee8f5ee1586eed0e706152f14014d Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Mon, 9 Sep 2024 21:11:32 +0100 Subject: [PATCH 03/14] fix --- .../tools/betting_strategies/market_moving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 92b104a8..f35c2811 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -65,7 +65,7 @@ def get_market_moving_bet( float(fixed_product), ) - new_p_yes = Probability(new_amounts[False] / sum(new_amounts)) + new_p_yes = Probability(new_amounts[False] / sum(list(new_amounts.values()))) if abs(target_p_yes - new_p_yes) < 0.01: break elif new_p_yes > target_p_yes: From e095b2753395eb0f304dd696311cf199d0e574a4 Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Mon, 9 Sep 2024 21:12:52 +0100 Subject: [PATCH 04/14] Review comment --- .../tools/betting_strategies/market_moving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f35c2811..9f378ed1 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -34,7 +34,7 @@ def get_market_moving_bet( dx = (new_product - fixed_product) / na_y """ fixed_product = yes_outcome_pool_size * no_outcome_pool_size - bet_direction: bool = True if target_p_yes > market_p_yes else False + bet_direction: bool = target_p_yes > market_p_yes min_bet_amount = 0.0 max_bet_amount = 100 * ( From 111b3cedcc6295e13cdb19a39644217c14e2828d Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Mon, 9 Sep 2024 21:14:29 +0100 Subject: [PATCH 05/14] Remove breakpoint --- .../tools/betting_strategies/kelly_criterion.py | 1 - 1 file changed, 1 deletion(-) 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 3323a276..a7d36f0b 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py @@ -131,7 +131,6 @@ def get_kelly_bet_full( ) denominator = 2 * (x**2 * f - y**2 * f) if denominator == 0: - breakpoint() return SimpleBet(direction=True, size=0) kelly_bet_amount = numerator / denominator From 5d5a681950cbfdbdce6b1c1a951d87d105f7130e Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Tue, 10 Sep 2024 00:54:55 +0100 Subject: [PATCH 06/14] Add sanity check to tests --- .../tools/betting_strategies/market_moving.py | 52 +++++++++++++++++++ tests/markets/test_betting_strategies.py | 27 ++++++++++ 2 files changed, 79 insertions(+) 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 9f378ed1..12ba48c5 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -3,7 +3,9 @@ import numpy as np from prediction_market_agent_tooling.gtypes import Probability +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.web3_utils import wei_to_xdai, xdai_to_wei def get_market_moving_bet( @@ -80,3 +82,53 @@ def get_market_moving_bet( max_bet_amount = bet_amount 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(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(buy_amount)) + + 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) + ] + 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/tests/markets/test_betting_strategies.py b/tests/markets/test_betting_strategies.py index 7fe1a118..13b8baf6 100644 --- a/tests/markets/test_betting_strategies.py +++ b/tests/markets/test_betting_strategies.py @@ -16,6 +16,7 @@ wei_type, xdai_type, ) +from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy from prediction_market_agent_tooling.markets.manifold.manifold import ( ManifoldAgentMarket, ) @@ -33,6 +34,7 @@ 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 ( @@ -190,6 +192,31 @@ def test_get_market_moving_bet( 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] + + 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) + ] + + 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( "probability, min_bet, max_bet, expected_bet", [ From d6f9005f970cae50401a6c2376451d116ca1b0db Mon Sep 17 00:00:00 2001 From: Evan Griffiths <56087052+evangriffiths@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:19:13 +0100 Subject: [PATCH 07/14] Update prediction_market_agent_tooling/deploy/betting_strategy.py Co-authored-by: Peter Jung --- prediction_market_agent_tooling/deploy/betting_strategy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index 455d1121..13f0f0c1 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -170,7 +170,6 @@ def calculate_trades( market: AgentMarket, ) -> list[Trade]: adjusted_bet_amount = self.adjust_bet_amount(existing_position, market) - # TODO use market.get_outcome_str_from_bool after https://github.com/gnosis/prediction-market-agent-tooling/pull/387 merges kelly_bet = ( get_kelly_bet_full( yes_outcome_pool_size=check_not_none(market.outcome_token_pool)[ From d50c5532b37872e983c812d7f825a35b4ef018bb Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Tue, 10 Sep 2024 09:34:12 +0100 Subject: [PATCH 08/14] mypy --- .../tools/betting_strategies/market_moving.py | 20 +++++++++---------- tests/markets/test_betting_strategies.py | 9 +++------ 2 files changed, 12 insertions(+), 17 deletions(-) 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 12ba48c5..8ef905a5 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -2,9 +2,10 @@ import numpy as np -from prediction_market_agent_tooling.gtypes import Probability +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 wei_to_xdai, xdai_to_wei @@ -92,20 +93,17 @@ def _sanity_check_omen_market_moving_bet( 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(bet_to_check.size), + 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(buy_amount)) - - 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) - ] + 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 diff --git a/tests/markets/test_betting_strategies.py b/tests/markets/test_betting_strategies.py index 13b8baf6..e2b53741 100644 --- a/tests/markets/test_betting_strategies.py +++ b/tests/markets/test_betting_strategies.py @@ -200,12 +200,9 @@ def test_sanity_check_market_moving_bet(target_p_yes: float) -> None: 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) - ] + 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, From 745bd1c5f97a4c79dfb729df7c76f0a0ce4dba1b Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Tue, 10 Sep 2024 09:39:36 +0100 Subject: [PATCH 09/14] fix --- prediction_market_agent_tooling/deploy/betting_strategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index 13f0f0c1..cd2978a2 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -179,12 +179,12 @@ def calculate_trades( market.get_outcome_str_from_bool(False) ], estimated_p_yes=answer.p_yes, - max_bet=self.max_bet_amount, + max_bet=adjusted_bet_amount, confidence=answer.confidence, ) if market.has_token_pool() else get_kelly_bet_simplified( - self.max_bet_amount, + adjusted_bet_amount, market.current_p_yes, answer.p_yes, answer.confidence, From 2038282895f7826932ae117920898ef6bf3031e3 Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Tue, 10 Sep 2024 10:28:52 +0100 Subject: [PATCH 10/14] tidy --- prediction_market_agent_tooling/deploy/betting_strategy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index cd2978a2..1137bf87 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -170,12 +170,13 @@ def calculate_trades( market: AgentMarket, ) -> list[Trade]: adjusted_bet_amount = self.adjust_bet_amount(existing_position, market) + outcome_token_pool = check_not_none(market.outcome_token_pool) kelly_bet = ( get_kelly_bet_full( - yes_outcome_pool_size=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=check_not_none(market.outcome_token_pool)[ + no_outcome_pool_size=outcome_token_pool[ market.get_outcome_str_from_bool(False) ], estimated_p_yes=answer.p_yes, From 368c1af2aea0ae97c55daadf81831807ff669937 Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Tue, 10 Sep 2024 15:35:21 +0100 Subject: [PATCH 11/14] Make market-moving bet more precise --- .../tools/betting_strategies/market_moving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ef905a5..32446ff2 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -69,7 +69,7 @@ def get_market_moving_bet( ) new_p_yes = Probability(new_amounts[False] / sum(list(new_amounts.values()))) - if abs(target_p_yes - new_p_yes) < 0.01: + if abs(target_p_yes - new_p_yes) < 1e-6: break elif new_p_yes > target_p_yes: if bet_direction: From 43b4b975cd5af32ed71809b884d2b8cd61d1365d Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Tue, 10 Sep 2024 16:45:14 +0100 Subject: [PATCH 12/14] Add zero-bet test --- tests/markets/test_betting_strategies.py | 41 +++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/markets/test_betting_strategies.py b/tests/markets/test_betting_strategies.py index e2b53741..7e22d9ca 100644 --- a/tests/markets/test_betting_strategies.py +++ b/tests/markets/test_betting_strategies.py @@ -163,7 +163,7 @@ def test_minimum_bet_to_win_manifold( @pytest.mark.parametrize( "target_p_yes, expected_bet_size, expected_bet_direction", [ - (Probability(0.1), xdai_type(25.32), False), + (Probability(0.1), xdai_type(23.19), False), (Probability(0.9), xdai_type(18.1), True), ], ) @@ -263,3 +263,42 @@ def test_kelly_bet(est_p_yes: Probability, omen_market: OmenMarket) -> None: ).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 From d2818bac1b94c2e81c8e03db127324172dbfafa5 Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Wed, 11 Sep 2024 15:14:10 +0100 Subject: [PATCH 13/14] Add util function OmenAgentMarket.get_new_p_yes --- .../markets/omen/omen.py | 38 +++++++++++++++++++ tests/markets/omen/test_omen.py | 36 ++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/prediction_market_agent_tooling/markets/omen/omen.py b/prediction_market_agent_tooling/markets/omen/omen.py index 37643be2..e057bcea 100644 --- a/prediction_market_agent_tooling/markets/omen/omen.py +++ b/prediction_market_agent_tooling/markets/omen/omen.py @@ -577,6 +577,44 @@ 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_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_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)) + 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/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) From a54abbed14fc66cfe2c96e15ecb28bf47febabe3 Mon Sep 17 00:00:00 2001 From: evangriffiths Date: Wed, 11 Sep 2024 15:58:45 +0100 Subject: [PATCH 14/14] Add OmenAgentMarket.get_buy_token_amount util function --- .../markets/omen/omen.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/prediction_market_agent_tooling/markets/omen/omen.py b/prediction_market_agent_tooling/markets/omen/omen.py index e057bcea..f73a98f3 100644 --- a/prediction_market_agent_tooling/markets/omen/omen.py +++ b/prediction_market_agent_tooling/markets/omen/omen.py @@ -577,6 +577,20 @@ 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. @@ -595,16 +609,7 @@ def get_new_p_yes(self, bet_amount: BetAmount, direction: bool) -> Probability: bet_amount.amount * (1 - self.fee) ) - 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)) + received_token_amount = self.get_buy_token_amount(bet_amount, direction).amount if direction: new_yes_outcome_pool_size -= received_token_amount else: