Skip to content

Commit

Permalink
Auto-verify that buy trades wouldn't cause guaranted loss (#488)
Browse files Browse the repository at this point in the history
  • Loading branch information
kongzii authored Oct 17, 2024
1 parent cc21ebf commit fb5bd28
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
25 changes: 24 additions & 1 deletion prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down
5 changes: 5 additions & 0 deletions prediction_market_agent_tooling/markets/agent_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
94 changes: 92 additions & 2 deletions tests/test_betting_strategy.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
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,
ProbabilisticAnswer,
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(
Expand All @@ -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={
Expand All @@ -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"
Expand All @@ -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."

0 comments on commit fb5bd28

Please sign in to comment.