Skip to content

Commit

Permalink
Add AgentMarket.get_positions_value (#283)
Browse files Browse the repository at this point in the history
  • Loading branch information
evangriffiths authored Jun 25, 2024
1 parent 7073d2d commit a467a31
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 2 deletions.
21 changes: 21 additions & 0 deletions prediction_market_agent_tooling/markets/agent_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ def get_last_trade_p_no(self) -> Probability | None:
"""
raise NotImplementedError("Subclasses must implement this method")

def get_last_trade_yes_outcome_price(self) -> float | None:
# Price on prediction markets are, by definition, equal to the probability of an outcome.
# Just making it explicit in this function.
if last_trade_p_yes := self.get_last_trade_p_yes():
return float(last_trade_p_yes)
return None

def get_last_trade_no_outcome_price(self) -> float | None:
# Price on prediction markets are, by definition, equal to the probability of an outcome.
# Just making it explicit in this function.
if last_trade_p_no := self.get_last_trade_p_no():
return float(last_trade_p_no)
return None

def get_bet_amount(self, amount: float) -> BetAmount:
return BetAmount(amount=amount, currency=self.currency)

Expand Down Expand Up @@ -193,6 +207,13 @@ def get_positions(cls, user_id: str, liquid_only: bool = False) -> list[Position
"""
raise NotImplementedError("Subclasses must implement this method")

@classmethod
def get_positions_value(cls, positions: list[Position]) -> BetAmount:
"""
Get the total value of all positions held by a user.
"""
raise NotImplementedError("Subclasses must implement this method")

def can_be_traded(self) -> bool:
if self.is_closed() or not self.has_liquidity():
return False
Expand Down
61 changes: 61 additions & 0 deletions prediction_market_agent_tooling/markets/omen/omen.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,67 @@ def get_positions(cls, user_id: str, liquid_only: bool = False) -> list[Position

return positions

@classmethod
def get_positions_value(cls, positions: list[Position]) -> BetAmount:
# Two dicts to map from market ids to (1) positions and (2) market.
market_ids_positions = {p.market_id: p for p in positions}
# Check there is only one position per market.
if len(set(market_ids_positions.keys())) != len(positions):
raise ValueError(
f"Markets for positions ({market_ids_positions.keys()}) are not unique."
)
markets: list[OmenAgentMarket] = [
OmenAgentMarket.from_data_model(m)
for m in OmenSubgraphHandler().get_omen_binary_markets(
limit=sys.maxsize, id_in=list(market_ids_positions.keys())
)
]
market_ids_markets = {m.id: m for m in markets}

# Validate that dict keys are the same.
if set(market_ids_positions.keys()) != set(market_ids_markets.keys()):
raise ValueError(
f"Market ids in {market_ids_positions.keys()} are not the same as in {market_ids_markets.keys()}"
)

# Initialise position value.
total_position_value = 0.0

for market_id in market_ids_positions.keys():
position = market_ids_positions[market_id]
market = market_ids_markets[market_id]

yes_tokens = 0.0
no_tokens = 0.0
if OMEN_TRUE_OUTCOME in position.amounts:
yes_tokens = position.amounts[OutcomeStr(OMEN_TRUE_OUTCOME)].amount
if OMEN_FALSE_OUTCOME in position.amounts:
no_tokens = position.amounts[OutcomeStr(OMEN_FALSE_OUTCOME)].amount

# Account for the value of positions in resolved markets
if market.is_resolved() and market.has_successful_resolution():
valued_tokens = yes_tokens if market.boolean_outcome else no_tokens
total_position_value += valued_tokens

# Or if the market is open and trading, get the value of the position
elif market.can_be_traded():
total_position_value += yes_tokens * market.yes_outcome_price
total_position_value += no_tokens * market.no_outcome_price

# Or if the market is still open but not trading, estimate the value
# of the position
else:
if yes_tokens:
yes_price = check_not_none(
market.get_last_trade_yes_outcome_price()
)
total_position_value += yes_tokens * yes_price
if no_tokens:
no_price = check_not_none(market.get_last_trade_no_outcome_price())
total_position_value += no_tokens * no_price

return BetAmount(amount=total_position_value, currency=Currency.xDai)

@classmethod
def get_user_url(cls, keys: APIKeys) -> str:
return f"https://gnosisscan.io/address/{keys.bet_from_address}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def _build_where_statements(
resolved: bool | None = None,
liquidity_bigger_than: Wei | None = None,
condition_id_in: list[HexBytes] | None = None,
id_in: list[str] | None = None,
excluded_questions: set[str] | None = None,
) -> dict[str, t.Any]:
where_stms: dict[str, t.Any] = {
Expand Down Expand Up @@ -193,6 +194,9 @@ def _build_where_statements(
if condition_id_in is not None:
where_stms["condition_"]["id_in"] = [x.hex() for x in condition_id_in]

if id_in is not None:
where_stms["id_in"] = id_in

if resolved is not None:
if resolved:
where_stms["resolutionTimestamp_not"] = None
Expand Down Expand Up @@ -308,6 +312,7 @@ def get_omen_binary_markets(
creator: t.Optional[HexAddress] = None,
liquidity_bigger_than: Wei | None = None,
condition_id_in: list[HexBytes] | None = None,
id_in: list[str] | None = None,
excluded_questions: set[str] | None = None, # question titles
sort_by_field: FieldPath | None = None,
sort_direction: str | None = None,
Expand All @@ -326,6 +331,7 @@ def get_omen_binary_markets(
finalized=finalized,
resolved=resolved,
condition_id_in=condition_id_in,
id_in=id_in,
excluded_questions=excluded_questions,
liquidity_bigger_than=liquidity_bigger_than,
)
Expand Down
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.39.0"
version = "0.39.1"
description = "Tools to benchmark, deploy and monitor prediction market agents."
authors = ["Gnosis"]
readme = "README.md"
Expand Down
45 changes: 44 additions & 1 deletion tests/markets/omen/test_omen.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from datetime import datetime

import numpy as np
from eth_account import Account
from eth_typing import HexAddress, HexStr
from web3 import Web3

from prediction_market_agent_tooling.gtypes import DatetimeWithTimezone, OutcomeStr
from prediction_market_agent_tooling.gtypes import DatetimeWithTimezone, OutcomeStr, Wei
from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
from prediction_market_agent_tooling.markets.data_models import Position, TokenAmount
from prediction_market_agent_tooling.markets.omen.data_models import OmenBet
from prediction_market_agent_tooling.markets.omen.omen import (
OmenAgentMarket,
get_binary_market_p_yes_history,
Expand All @@ -14,6 +18,7 @@
OmenSubgraphHandler,
)
from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow
from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai


def test_omen_pick_binary_market() -> None:
Expand Down Expand Up @@ -145,3 +150,41 @@ def test_get_positions_1() -> None:
assert token_balance.amount == position.amounts[OutcomeStr(outcome_str)].amount

print(position) # For extra test coverage


def test_positions_value() -> None:
"""
Test that an artificial user position (generated based on a historical
resolved bet) has the correct expected value, based on the bet's profit
"""
user_address = Web3.to_checksum_address(
"0x2DD9f5678484C1F59F97eD334725858b938B4102"
)
resolved_bets = OmenSubgraphHandler().get_resolved_bets_with_valid_answer(
start_time=datetime(2024, 3, 27, 4, 20),
end_time=datetime(2024, 3, 27, 4, 30),
better_address=user_address,
)
assert len(resolved_bets) == 1
bet = resolved_bets[0]
assert bet.to_generic_resolved_bet().is_correct

def bet_to_position(bet: OmenBet) -> Position:
market = OmenAgentMarket.get_binary_market(bet.fpmm.id)
outcome_str = OutcomeStr(market.get_outcome_str(bet.outcomeIndex))
outcome_tokens = TokenAmount(
amount=wei_to_xdai(Wei(bet.outcomeTokensTraded)),
currency=OmenAgentMarket.currency,
)
return Position(market_id=market.id, amounts={outcome_str: outcome_tokens})

positions = [bet_to_position(bet)]
position_value = OmenAgentMarket.get_positions_value(positions=positions)

bet_value_amount = bet.get_profit().amount + wei_to_xdai(bet.collateralAmount)
assert np.isclose(
position_value.amount,
bet_value_amount,
rtol=1e-3, # relax tolerances due to fees
atol=1e-3,
)
11 changes: 11 additions & 0 deletions tests/markets/omen/test_omen_subgraph_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,14 @@ def build_incomplete_user_position_from_condition_ids(
conditionIds=condition_ids,
)
)


def test_get_markets_id_in() -> None:
market_id = "0x934b9f379dd9d8850e468df707d58711da2966cd"
sgh = OmenSubgraphHandler()
markets = sgh.get_omen_binary_markets(
limit=1,
id_in=[market_id],
)
assert len(markets) == 1
assert markets[0].id == market_id

0 comments on commit a467a31

Please sign in to comment.