Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-verify that buy trades wouldn't cause guaranted loss #488

Merged
merged 7 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=}."
)

kongzii marked this conversation as resolved.
Show resolved Hide resolved
@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")
kongzii marked this conversation as resolved.
Show resolved Hide resolved

def sell_tokens(self, outcome: bool, amount: TokenAmount) -> str:
raise NotImplementedError("Subclasses must implement this method")
Comment on lines 184 to 185
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring buy_tokens to use get_buy_token_amount.

The buy_tokens method could potentially be refactored to use the newly added get_buy_token_amount method. This would improve consistency and reduce potential duplication of logic. Here's a suggested implementation:

def buy_tokens(self, outcome: bool, amount: TokenAmount) -> str:
    bet_amount = self.get_bet_amount(amount.amount)
    token_amount = self.get_buy_token_amount(bet_amount, outcome)
    return self.place_bet(outcome=outcome, amount=token_amount)

This refactoring assumes that get_buy_token_amount will be implemented in subclasses to handle the conversion from BetAmount to TokenAmount. Please verify if this aligns with the intended usage of get_buy_token_amount.


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."
Loading