From fb5bd28da519fcd802aa61827dfb5c795f070930 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Thu, 17 Oct 2024 15:31:23 +0200 Subject: [PATCH] Auto-verify that buy trades wouldn't cause guaranted loss (#488) --- .../deploy/betting_strategy.py | 25 ++++- .../markets/agent_market.py | 5 + tests/test_betting_strategy.py | 94 ++++++++++++++++++- 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index 07b04220..8e9b3532 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -47,6 +47,26 @@ def assert_trades_currency_match_markets( "Cannot handle trades with currencies that deviate from market's currency" ) + @staticmethod + def assert_buy_trade_wont_be_guaranteed_loss( + market: AgentMarket, trades: list[Trade] + ) -> None: + for trade in trades: + if trade.trade_type == TradeType.BUY: + outcome_tokens_to_get = market.get_buy_token_amount( + trade.amount, trade.outcome + ) + + if outcome_tokens_to_get.amount < trade.amount.amount: + raise RuntimeError( + f"Trade {trade=} would result in guaranteed loss by getting only {outcome_tokens_to_get=}." + ) + + @staticmethod + def check_trades(market: AgentMarket, trades: list[Trade]) -> None: + BettingStrategy.assert_trades_currency_match_markets(market, trades) + BettingStrategy.assert_buy_trade_wont_be_guaranteed_loss(market, trades) + def _build_rebalance_trades_from_positions( self, existing_position: Position | None, @@ -95,7 +115,10 @@ def _build_rebalance_trades_from_positions( # Sort inplace with SELL last trades.sort(key=lambda t: t.trade_type == TradeType.SELL) - BettingStrategy.assert_trades_currency_match_markets(market, trades) + + # Run some sanity checks to not place unreasonable bets. + BettingStrategy.check_trades(market, trades) + return trades diff --git a/prediction_market_agent_tooling/markets/agent_market.py b/prediction_market_agent_tooling/markets/agent_market.py index ccd1fed7..252ffc61 100644 --- a/prediction_market_agent_tooling/markets/agent_market.py +++ b/prediction_market_agent_tooling/markets/agent_market.py @@ -176,6 +176,11 @@ def place_bet(self, outcome: bool, amount: BetAmount) -> str: def buy_tokens(self, outcome: bool, amount: TokenAmount) -> str: return self.place_bet(outcome=outcome, amount=amount) + def get_buy_token_amount( + self, bet_amount: BetAmount, direction: bool + ) -> TokenAmount: + raise NotImplementedError("Subclasses must implement this method") + def sell_tokens(self, outcome: bool, amount: TokenAmount) -> str: raise NotImplementedError("Subclasses must implement this method") diff --git a/tests/test_betting_strategy.py b/tests/test_betting_strategy.py index 0790e592..1731d103 100644 --- a/tests/test_betting_strategy.py +++ b/tests/test_betting_strategy.py @@ -1,11 +1,19 @@ +from datetime import timedelta from unittest.mock import Mock import pytest +from web3 import Web3 from prediction_market_agent_tooling.deploy.betting_strategy import ( + BettingStrategy, MaxAccuracyBettingStrategy, ) -from prediction_market_agent_tooling.gtypes import Probability +from prediction_market_agent_tooling.gtypes import ( + HexAddress, + HexBytes, + HexStr, + Probability, +) from prediction_market_agent_tooling.markets.data_models import ( Currency, Position, @@ -13,7 +21,18 @@ TokenAmount, TradeType, ) -from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket +from prediction_market_agent_tooling.markets.omen.data_models import ( + OMEN_BINARY_MARKET_OUTCOMES, +) +from prediction_market_agent_tooling.markets.omen.omen import ( + Condition, + MarketFees, + OmenAgentMarket, +) +from prediction_market_agent_tooling.markets.omen.omen_contracts import ( + WrappedxDaiContract, +) +from prediction_market_agent_tooling.tools.utils import utcnow @pytest.mark.parametrize( @@ -34,6 +53,7 @@ def test_answer_decision( def test_rebalance() -> None: tiny_amount = TokenAmount(amount=0.0001, currency=Currency.xDai) mock_amount = TokenAmount(amount=5, currency=Currency.xDai) + liquidity_amount = TokenAmount(amount=100, currency=Currency.xDai) mock_existing_position = Position( market_id="0x123", amounts={ @@ -42,10 +62,13 @@ def test_rebalance() -> None: }, ) bet_amount = tiny_amount.amount + mock_existing_position.total_amount.amount + buy_token_amount = TokenAmount(amount=10, currency=Currency.xDai) strategy = MaxAccuracyBettingStrategy(bet_amount=bet_amount) mock_answer = ProbabilisticAnswer(p_yes=Probability(0.9), confidence=0.5) mock_market = Mock(OmenAgentMarket, wraps=OmenAgentMarket) + mock_market.get_liquidity.return_value = liquidity_amount mock_market.get_tiny_bet_amount.return_value = tiny_amount + mock_market.get_buy_token_amount.return_value = buy_token_amount mock_market.current_p_yes = 0.5 mock_market.currency = Currency.xDai mock_market.id = "0x123" @@ -60,3 +83,70 @@ def test_rebalance() -> None: sell_trade = trades[1] assert sell_trade.trade_type == TradeType.SELL assert sell_trade.amount.amount == mock_amount.amount + + +@pytest.mark.parametrize( + "strategy, liquidity, bet_proportion_fee, should_raise", + [ + ( + MaxAccuracyBettingStrategy(bet_amount=100), + 1, + 0.02, + True, # Should raise because fee will eat the profit. + ), + ( + MaxAccuracyBettingStrategy(bet_amount=100), + 10, + 0.02, + False, # Should be okay, because liquidity + fee combo is reasonable. + ), + ( + MaxAccuracyBettingStrategy(bet_amount=100), + 10, + 0.5, + True, # Should raise because fee will eat the profit. + ), + ], +) +def test_attacking_market( + strategy: BettingStrategy, + liquidity: float, + bet_proportion_fee: float, + should_raise: bool, +) -> None: + """ + Test if markets with unreasonably low liquidity and/or high fees won't put agent into immediate loss. + """ + market = OmenAgentMarket( + id="0x0", + question="How you doing?", + outcomes=OMEN_BINARY_MARKET_OUTCOMES, + resolution=None, + url="", + volume=None, + creator=HexAddress(HexStr("0x0")), + collateral_token_contract_address_checksummed=WrappedxDaiContract().address, + market_maker_contract_address_checksummed=Web3.to_checksum_address( + "0x0000000000000000000000000000000000000001" + ), + condition=Condition( + id=HexBytes("0x0"), outcomeSlotCount=len(OMEN_BINARY_MARKET_OUTCOMES) + ), + finalized_time=None, + created_time=utcnow(), + close_time=utcnow() + timedelta(days=3), + current_p_yes=Probability(0.5), + outcome_token_pool={ + OMEN_BINARY_MARKET_OUTCOMES[0]: liquidity, + OMEN_BINARY_MARKET_OUTCOMES[1]: liquidity, + }, + fees=MarketFees.get_zero_fees(bet_proportion=bet_proportion_fee), + ) + answer = ProbabilisticAnswer(p_yes=Probability(0.9), confidence=1.0) + + try: + trades = strategy.calculate_trades(None, answer, market) + assert not should_raise, "Should not have raised and return trades normally." + assert trades, "No trades available." + except Exception: + assert should_raise, "Should have raise to prevent placing of bet."