Skip to content

Commit

Permalink
OmenAgentMarket.sell_tokens now correctly takes token/share number as…
Browse files Browse the repository at this point in the history
… arg (#264)

* OmenAgentMarket.sell_tokens now correctly takes token/share number as arg
  • Loading branch information
evangriffiths authored Jun 6, 2024
1 parent 7c5bffd commit 3fe4470
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 53 deletions.
39 changes: 37 additions & 2 deletions prediction_market_agent_tooling/markets/omen/omen.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ChecksumAddress,
HexAddress,
HexStr,
OmenOutcomeToken,
OutcomeStr,
Probability,
Wei,
Expand Down Expand Up @@ -54,7 +55,10 @@
)
from prediction_market_agent_tooling.tools.balances import get_balances
from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
from prediction_market_agent_tooling.tools.utils import check_not_none
from prediction_market_agent_tooling.tools.utils import (
calculate_sell_amount_in_collateral,
check_not_none,
)
from prediction_market_agent_tooling.tools.web3_utils import (
add_fraction,
remove_fraction,
Expand All @@ -80,6 +84,8 @@ class OmenAgentMarket(AgentMarket):
finalized_time: datetime | None
created_time: datetime
close_time: datetime
outcome_token_amounts: list[OmenOutcomeToken]
fee: float # proportion, from 0 to 1

INVALID_MARKET_ANSWER: HexStr = HexStr(
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
Expand Down Expand Up @@ -155,23 +161,50 @@ def place_bet(
web3=web3,
)

def calculate_sell_amount_in_collateral(
self, amount: TokenAmount, outcome: bool
) -> xDai:
if len(self.outcome_token_amounts) != 2:
raise ValueError(
f"Market {self.id} has {len(self.outcome_token_amounts)} "
f"outcomes. This method only supports binary markets."
)
sell_index = self.yes_index if outcome else self.no_index
other_index = self.no_index if outcome else self.yes_index
collateral = calculate_sell_amount_in_collateral(
shares_to_sell=amount.amount,
holdings=wei_to_xdai(Wei(self.outcome_token_amounts[sell_index])),
other_holdings=wei_to_xdai(Wei(self.outcome_token_amounts[other_index])),
fee=self.fee,
)
return xDai(collateral)

def sell_tokens(
self,
outcome: bool,
amount: TokenAmount,
auto_withdraw: bool = False,
api_keys: APIKeys | None = None,
web3: Web3 | None = None,
) -> None:
if not self.can_be_traded():
raise ValueError(
f"Market {self.id} is not open for trading. Cannot sell tokens."
)

# Convert from token (i.e. share) number to xDai value of tokens, as
# this is the expected unit of the argument in the smart contract.
collateral = self.calculate_sell_amount_in_collateral(
amount=amount,
outcome=outcome,
)
binary_omen_sell_outcome_tx(
amount=collateral,
api_keys=api_keys if api_keys is not None else APIKeys(),
amount=xDai(amount.amount),
market=self,
binary_outcome=outcome,
auto_withdraw=auto_withdraw,
web3=web3,
)

def was_any_bet_outcome_correct(
Expand Down Expand Up @@ -249,6 +282,8 @@ def from_data_model(model: OmenMarket) -> "OmenAgentMarket":
url=model.url,
volume=wei_to_xdai(model.collateralVolume),
close_time=model.close_time,
outcome_token_amounts=model.outcomeTokenAmounts,
fee=float(wei_to_xdai(model.fee)) if model.fee is not None else 0.0,
)

@staticmethod
Expand Down
33 changes: 33 additions & 0 deletions prediction_market_agent_tooling/tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytz
import requests
from pydantic import BaseModel, ValidationError
from scipy.optimize import newton
from scipy.stats import entropy

from prediction_market_agent_tooling.gtypes import (
Expand Down Expand Up @@ -173,3 +174,35 @@ def prob_uncertainty(prob: Probability) -> float:
- Market's probability is 0.95, so the market is quite certain about YES: prob_uncertainty(0.95) == 0.286
"""
return float(entropy([prob, 1 - prob], base=2))


def calculate_sell_amount_in_collateral(
shares_to_sell: float,
holdings: float,
other_holdings: float,
fee: float,
) -> float:
"""
Computes the amount of collateral that needs to be sold to get `shares`
amount of shares. Returns None if the amount can't be computed.
Taken from https://github.com/protofire/omen-exchange/blob/29d0ab16bdafa5cc0d37933c1c7608a055400c73/app/src/util/tools/fpmm/trading/index.ts#L99
Simplified for binary markets.
"""

if not (0 <= fee < 1.0):
raise ValueError("Fee must be between 0 and 1")

for v in [shares_to_sell, holdings, other_holdings]:
if v <= 0:
raise ValueError("All share args must be greater than 0")

def f(r: float) -> float:
R = r / (1 - fee)
first_term = other_holdings - R
second_term = holdings + shares_to_sell - R
third_term = holdings * other_holdings
return (first_term * second_term) - third_term

amount_to_sell = newton(f, 0)
return float(amount_to_sell) * 0.999999 # Avoid rounding errors
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "prediction-market-agent-tooling"
version = "0.34.0"
version = "0.35.0"
description = "Tools to benchmark, deploy and monitor prediction market agents."
authors = ["Gnosis"]
readme = "README.md"
Expand Down
44 changes: 14 additions & 30 deletions scripts/sell_all_omen.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def sell_all(
)
bets_total_usd = sum(b.collateralAmountUSD for b in bets)
unique_market_urls = set(b.fpmm.url for b in bets)
balances_before = get_balances(better_address)
starting_balance = get_balances(better_address)
new_balance = starting_balance # initialisation

logger.info(
f"For {better_address}, found the following {len(bets)} bets on {len(unique_market_urls)} unique markets worth of {bets_total_usd} USD: {unique_market_urls}"
Expand All @@ -66,37 +67,20 @@ def sell_all(
)
continue

# TODO: This should be fixable once https://github.com/gnosis/prediction-market-agent-tooling/issues/195 is resolved and used in this script.
# We need to convert `current_token_balance.amount` properly into their actual xDai value and then use it here.
# Afterwards, we can sell the actual xDai value, instead of just trying to sell these hard-coded values.
for current_xdai_value in [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1]:
current_token_balance.amount = current_xdai_value
old_balance = new_balance
agent_market.sell_tokens(
bet.boolean_outcome,
current_token_balance,
auto_withdraw=auto_withdraw,
api_keys=api_keys,
)
new_balance = get_balances(better_address)

try:
agent_market.sell_tokens(
bet.boolean_outcome,
current_token_balance,
auto_withdraw=auto_withdraw,
api_keys=api_keys,
)
logger.info(
f"Sold bet on {bet.fpmm.url} for {current_xdai_value} xDai."
)
break
except Exception as e:
# subtraction error is currently expected because of the TODO above, so log only other errors
if "Reverted SafeMath: subtraction overflow" not in str(e):
logger.error(
f"Failed to sell bet on {bet.fpmm.url} for {current_xdai_value} xDai because of {e}."
)
continue
else:
logger.warning(
f"Skipped bet on {bet.fpmm.url} because of insufficient balance."
)
logger.info(
f"Sold bet on {bet.fpmm.url} for {new_balance.wxdai - old_balance.wxdai} xDai."
)

balances_after = get_balances(better_address)
logger.info(f"Obtained back {balances_after.wxdai - balances_before.wxdai} wxDai.")
logger.info(f"Obtained back {new_balance.wxdai - starting_balance.wxdai} wxDai.")


if __name__ == "__main__":
Expand Down
2 changes: 2 additions & 0 deletions tests/markets/test_betting_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ def test_minimum_bet_to_win(
url="url",
volume=None,
finalized_time=None,
outcome_token_amounts=[OmenOutcomeToken(2), OmenOutcomeToken(3)],
fee=0.02,
),
)
assert (
Expand Down
83 changes: 83 additions & 0 deletions tests/tools/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import numpy as np
import pytest

from prediction_market_agent_tooling.tools.utils import (
calculate_sell_amount_in_collateral,
)


def test_calculate_sell_amount_in_collateral_0() -> None:
# Sanity check: If the market is 50/50, then the collateral value of one
# share is 0.5
collateral = calculate_sell_amount_in_collateral(
shares_to_sell=1,
holdings=1000000000000 - 1,
other_holdings=1000000000000,
fee=0,
)
assert np.isclose(collateral, 0.5)


def test_calculate_sell_amount_in_collateral_1() -> None:
# Sanity check that shares have near-zero value with this ratio
near_zero_collateral = calculate_sell_amount_in_collateral(
shares_to_sell=1,
holdings=10000000000000,
other_holdings=1,
fee=0,
)
assert np.isclose(near_zero_collateral, 0)

# Sanity check that shares have near-one value with this ratio
near_zero_collateral = calculate_sell_amount_in_collateral(
shares_to_sell=1,
holdings=1,
other_holdings=10000000000000,
fee=0,
)
assert np.isclose(near_zero_collateral, 1)


def test_calculate_sell_amount_in_collateral_2() -> None:
# Sanity check: the value of sold shares decreases as the fee increases
def get_collateral(fee: float) -> float:
return calculate_sell_amount_in_collateral(
shares_to_sell=2.5,
holdings=10,
other_holdings=3,
fee=fee,
)

c1 = get_collateral(fee=0.1)
c2 = get_collateral(fee=0.35)
assert c1 > c2


def test_calculate_sell_amount_in_collateral_3() -> None:
# Check error handling when fee is invalid
def get_collateral(fee: float) -> float:
return calculate_sell_amount_in_collateral(
shares_to_sell=2.5,
holdings=10,
other_holdings=3,
fee=fee,
)

with pytest.raises(ValueError) as e:
get_collateral(fee=-0.1)
assert str(e.value) == "Fee must be between 0 and 1"

with pytest.raises(ValueError) as e:
get_collateral(fee=1.0)
assert str(e.value) == "Fee must be between 0 and 1"


def test_calculate_sell_amount_in_collateral_4() -> None:
with pytest.raises(ValueError) as e:
collateral = calculate_sell_amount_in_collateral(
shares_to_sell=100,
holdings=10,
other_holdings=0,
fee=0,
)
assert str(e.value) == "All share args must be greater than 0"
57 changes: 37 additions & 20 deletions tests_integration/markets/omen/test_omen.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import time
from datetime import timedelta

import numpy as np
import pytest
from eth_typing import HexAddress, HexStr
from web3 import Web3
Expand All @@ -17,7 +19,6 @@
OMEN_DEFAULT_MARKET_FEE,
OmenAgentMarket,
binary_omen_buy_outcome_tx,
binary_omen_sell_outcome_tx,
omen_create_market_tx,
omen_fund_market_tx,
omen_redeem_full_position_tx,
Expand All @@ -30,6 +31,7 @@
from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
OmenSubgraphHandler,
)
from prediction_market_agent_tooling.tools.balances import get_balances
from prediction_market_agent_tooling.tools.utils import utcnow
from prediction_market_agent_tooling.tools.web3_utils import xdai_to_wei
from tests_integration.conftest import is_contract
Expand Down Expand Up @@ -220,25 +222,40 @@ def test_omen_buy_and_sell_outcome(
# Tests both buying and selling, so we are back at the square one in the wallet (minues fees).
# You can double check your address at https://gnosisscan.io/ afterwards.
market = OmenAgentMarket.from_data_model(pick_binary_market())
buy_amount = xdai_type(0.00142)
sell_amount = xdai_type(
buy_amount / 2
) # There will be some fees, so this has to be lower.

binary_omen_buy_outcome_tx(
api_keys=test_keys,
amount=buy_amount,
market=market,
binary_outcome=True,
auto_deposit=True,
outcome = True
outcome_str = OMEN_TRUE_OUTCOME if outcome else OMEN_FALSE_OUTCOME
bet_amount = market.get_bet_amount(amount=0.4)

# TODO hack until https://github.com/gnosis/prediction-market-agent-tooling/issues/266 is complete
os.environ[
"BET_FROM_PRIVATE_KEY"
] = test_keys.bet_from_private_key.get_secret_value()
api_keys = APIKeys()

def get_market_outcome_tokens() -> TokenAmount:
return market.get_token_balance(
user_id=api_keys.bet_from_address,
outcome=outcome_str,
web3=local_web3,
)

# Check our wallet has sufficient funds
balances = get_balances(address=api_keys.bet_from_address, web3=local_web3)
assert balances.xdai + balances.wxdai > bet_amount.amount

market.place_bet(outcome=outcome, amount=bet_amount, web3=local_web3)

# Check that we now have a position in the market.
outcome_tokens = get_market_outcome_tokens()
assert outcome_tokens.amount > 0

market.sell_tokens(
outcome=outcome,
amount=outcome_tokens,
web3=local_web3,
api_keys=api_keys,
)

binary_omen_sell_outcome_tx(
api_keys=test_keys,
amount=sell_amount,
market=market,
binary_outcome=True,
auto_withdraw=True,
web3=local_web3,
)
# Check that we have sold our entire stake in the market.
remaining_tokens = get_market_outcome_tokens()
assert np.isclose(remaining_tokens.amount, 0, atol=5e-3)

0 comments on commit 3fe4470

Please sign in to comment.