From 45601b68c827206a85dcf194732c1f3990fd7509 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Thu, 19 Sep 2024 12:37:28 +0200 Subject: [PATCH] Getting jobs (#409) --- prediction_market_agent_tooling/gtypes.py | 2 +- .../jobs/__init__.py | 0 prediction_market_agent_tooling/jobs/jobs.py | 45 +++++++ .../jobs/jobs_models.py | 53 ++++++++ .../jobs/omen/omen_jobs.py | 113 ++++++++++++++++++ .../markets/omen/data_models.py | 3 + .../markets/omen/omen.py | 4 - .../markets/omen/omen_subgraph_handler.py | 25 ++-- pyproject.toml | 2 +- scripts/create_market_omen.py | 5 +- tests/markets/test_betting_strategies.py | 6 +- .../markets/omen/test_omen.py | 7 +- 12 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 prediction_market_agent_tooling/jobs/__init__.py create mode 100644 prediction_market_agent_tooling/jobs/jobs.py create mode 100644 prediction_market_agent_tooling/jobs/jobs_models.py create mode 100644 prediction_market_agent_tooling/jobs/omen/omen_jobs.py diff --git a/prediction_market_agent_tooling/gtypes.py b/prediction_market_agent_tooling/gtypes.py index a1c41ad5..a612b678 100644 --- a/prediction_market_agent_tooling/gtypes.py +++ b/prediction_market_agent_tooling/gtypes.py @@ -27,7 +27,7 @@ xDai = NewType("xDai", float) GNO = NewType("GNO", float) ABI = NewType("ABI", str) -OmenOutcomeToken = NewType("OmenOutcomeToken", int) +OmenOutcomeToken = NewType("OmenOutcomeToken", Wei) OutcomeStr = NewType("OutcomeStr", str) Probability = NewType("Probability", float) Mana = NewType("Mana", float) # Manifold's "currency" diff --git a/prediction_market_agent_tooling/jobs/__init__.py b/prediction_market_agent_tooling/jobs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prediction_market_agent_tooling/jobs/jobs.py b/prediction_market_agent_tooling/jobs/jobs.py new file mode 100644 index 00000000..e21d5f14 --- /dev/null +++ b/prediction_market_agent_tooling/jobs/jobs.py @@ -0,0 +1,45 @@ +import typing as t + +from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket +from prediction_market_agent_tooling.jobs.omen.omen_jobs import OmenJobAgentMarket +from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy +from prediction_market_agent_tooling.markets.markets import MarketType + +JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET: dict[MarketType, type[JobAgentMarket]] = { + MarketType.OMEN: OmenJobAgentMarket, +} + + +@t.overload +def get_jobs( + market_type: t.Literal[MarketType.OMEN], + limit: int | None, + filter_by: FilterBy = FilterBy.OPEN, + sort_by: SortBy = SortBy.NONE, +) -> t.Sequence[OmenJobAgentMarket]: + ... + + +@t.overload +def get_jobs( + market_type: MarketType, + limit: int | None, + filter_by: FilterBy = FilterBy.OPEN, + sort_by: SortBy = SortBy.NONE, +) -> t.Sequence[JobAgentMarket]: + ... + + +def get_jobs( + market_type: MarketType, + limit: int | None, + filter_by: FilterBy = FilterBy.OPEN, + sort_by: SortBy = SortBy.NONE, +) -> t.Sequence[JobAgentMarket]: + job_class = JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET[market_type] + markets = job_class.get_jobs( + limit=limit, + sort_by=sort_by, + filter_by=filter_by, + ) + return markets diff --git a/prediction_market_agent_tooling/jobs/jobs_models.py b/prediction_market_agent_tooling/jobs/jobs_models.py new file mode 100644 index 00000000..fa6b2817 --- /dev/null +++ b/prediction_market_agent_tooling/jobs/jobs_models.py @@ -0,0 +1,53 @@ +import typing as t +from abc import ABC, abstractmethod +from datetime import datetime + +from pydantic import BaseModel + +from prediction_market_agent_tooling.markets.agent_market import AgentMarket +from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import ( + FilterBy, + SortBy, +) + + +class SimpleJob(BaseModel): + id: str + job: str + reward: float + currency: str + deadline: datetime + + +class JobAgentMarket(AgentMarket, ABC): + CATEGORY: t.ClassVar[str] + + @property + @abstractmethod + def job(self) -> str: + """Holds description of the job that needs to be done.""" + + @property + @abstractmethod + def deadline(self) -> datetime: + """Deadline for the job completion.""" + + @abstractmethod + def get_reward(self, max_bond: float) -> float: + """Reward for completing this job.""" + + @abstractmethod + @classmethod + def get_jobs( + cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy + ) -> t.Sequence["JobAgentMarket"]: + """Get all available jobs.""" + + def to_simple_job(self, max_bond: float) -> SimpleJob: + return SimpleJob( + id=self.id, + job=self.job, + reward=self.get_reward(max_bond), + currency=self.currency.value, + deadline=self.deadline, + ) diff --git a/prediction_market_agent_tooling/jobs/omen/omen_jobs.py b/prediction_market_agent_tooling/jobs/omen/omen_jobs.py new file mode 100644 index 00000000..675d4184 --- /dev/null +++ b/prediction_market_agent_tooling/jobs/omen/omen_jobs.py @@ -0,0 +1,113 @@ +import typing as t +from datetime import datetime + +from web3 import Web3 + +from prediction_market_agent_tooling.deploy.betting_strategy import ( + Currency, + KellyBettingStrategy, + ProbabilisticAnswer, + TradeType, +) +from prediction_market_agent_tooling.gtypes import Probability +from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket +from prediction_market_agent_tooling.markets.omen.omen import ( + BetAmount, + OmenAgentMarket, + OmenMarket, +) +from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import ( + FilterBy, + OmenSubgraphHandler, + SortBy, +) + + +class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket): + CATEGORY = "jobs" + + @property + def job(self) -> str: + """Omen market's have only question, so that's where the job description is.""" + return self.question + + @property + def deadline(self) -> datetime: + return self.close_time + + def get_reward(self, max_bond: float) -> float: + return compute_job_reward(self, max_bond) + + @classmethod + def get_jobs( + cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy + ) -> t.Sequence["OmenJobAgentMarket"]: + markets = OmenSubgraphHandler().get_omen_binary_markets_simple( + limit=limit, + filter_by=filter_by, + sort_by=sort_by, + category=cls.CATEGORY, + ) + return [OmenJobAgentMarket.from_omen_market(market) for market in markets] + + @staticmethod + def from_omen_market(market: OmenMarket) -> "OmenJobAgentMarket": + return OmenJobAgentMarket.from_omen_agent_market( + OmenAgentMarket.from_data_model(market) + ) + + @staticmethod + def from_omen_agent_market(market: OmenAgentMarket) -> "OmenJobAgentMarket": + return OmenJobAgentMarket( + id=market.id, + question=market.question, + description=market.description, + outcomes=market.outcomes, + outcome_token_pool=market.outcome_token_pool, + resolution=market.resolution, + created_time=market.created_time, + close_time=market.close_time, + current_p_yes=market.current_p_yes, + url=market.url, + volume=market.volume, + creator=market.creator, + collateral_token_contract_address_checksummed=market.collateral_token_contract_address_checksummed, + market_maker_contract_address_checksummed=market.market_maker_contract_address_checksummed, + condition=market.condition, + finalized_time=market.finalized_time, + fee=market.fee, + ) + + +def compute_job_reward( + market: OmenAgentMarket, max_bond: float, web3: Web3 | None = None +) -> float: + # Because jobs are powered by prediction markets, potentional reward depends on job's liquidity and our will to bond (bet) our xDai into our job completion. + required_trades = KellyBettingStrategy(max_bet_amount=max_bond).calculate_trades( + existing_position=None, + # We assume that we finish the job and so the probability of the market happening will be 100%. + answer=ProbabilisticAnswer(p_yes=Probability(1.0), confidence=1.0), + market=market, + ) + + assert ( + len(required_trades) == 1 + ), f"Shouldn't process same job twice: {required_trades}" + trade = required_trades[0] + assert trade.trade_type == TradeType.BUY, "Should only buy on job markets." + assert trade.outcome, "Should buy only YES on job markets." + assert ( + trade.amount.currency == Currency.xDai + ), "Should work only on real-money markets." + + reward = ( + market.get_buy_token_amount( + bet_amount=BetAmount( + amount=trade.amount.amount, currency=trade.amount.currency + ), + direction=trade.outcome, + ).amount + - trade.amount.amount + ) + + return reward diff --git a/prediction_market_agent_tooling/markets/omen/data_models.py b/prediction_market_agent_tooling/markets/omen/data_models.py index 0b1074a7..d1cc178c 100644 --- a/prediction_market_agent_tooling/markets/omen/data_models.py +++ b/prediction_market_agent_tooling/markets/omen/data_models.py @@ -9,6 +9,7 @@ ChecksumAddress, HexAddress, HexBytes, + HexStr, OmenOutcomeToken, Probability, Wei, @@ -30,8 +31,10 @@ OMEN_TRUE_OUTCOME = "Yes" OMEN_FALSE_OUTCOME = "No" +OMEN_BINARY_MARKET_OUTCOMES = [OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME] INVALID_ANSWER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF INVALID_ANSWER_HEX_BYTES = HexBytes(INVALID_ANSWER) +INVALID_ANSWER_STR = HexStr(INVALID_ANSWER_HEX_BYTES.hex()) OMEN_BASE_URL = "https://aiomen.eth.limo" PRESAGIO_BASE_URL = "https://presagio.pages.dev" diff --git a/prediction_market_agent_tooling/markets/omen/omen.py b/prediction_market_agent_tooling/markets/omen/omen.py index 7be51832..b4f1b460 100644 --- a/prediction_market_agent_tooling/markets/omen/omen.py +++ b/prediction_market_agent_tooling/markets/omen/omen.py @@ -97,10 +97,6 @@ class OmenAgentMarket(AgentMarket): close_time: datetime fee: float # proportion, from 0 to 1 - INVALID_MARKET_ANSWER: HexStr = HexStr( - "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" - ) - _binary_market_p_yes_history: list[Probability] | None = None description: str | None = ( None # Omen markets don't have a description, so just default to None. diff --git a/prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py b/prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py index df02b994..5eff3f10 100644 --- a/prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +++ b/prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py @@ -19,8 +19,7 @@ from prediction_market_agent_tooling.loggers import logger from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy from prediction_market_agent_tooling.markets.omen.data_models import ( - OMEN_FALSE_OUTCOME, - OMEN_TRUE_OUTCOME, + OMEN_BINARY_MARKET_OUTCOMES, OmenBet, OmenMarket, OmenPosition, @@ -204,7 +203,7 @@ def _build_where_statements( self, creator: t.Optional[HexAddress] = None, creator_in: t.Optional[t.Sequence[HexAddress]] = None, - outcomes: list[str] = [OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME], + outcomes: list[str] = OMEN_BINARY_MARKET_OUTCOMES, created_after: t.Optional[datetime] = None, opened_before: t.Optional[datetime] = None, opened_after: t.Optional[datetime] = None, @@ -217,6 +216,7 @@ def _build_where_statements( id_in: list[str] | None = None, excluded_questions: set[str] | None = None, collateral_token_address_in: tuple[ChecksumAddress, ...] | None = None, + category: str | None = None, ) -> dict[str, t.Any]: where_stms: dict[str, t.Any] = { "isPendingArbitration": False, @@ -282,6 +282,9 @@ def _build_where_statements( finalized_after ) + if category: + where_stms["category"] = category + # `excluded_question_titles` can not be an empty list, otherwise the API bugs out and returns nothing. excluded_question_titles = [""] if excluded_questions: @@ -324,8 +327,10 @@ def get_omen_binary_markets_simple( # Additional filters, these can not be modified by the enums above. created_after: datetime | None = None, excluded_questions: set[str] | None = None, # question titles - collateral_token_address_in: tuple[ChecksumAddress, ...] - | None = SAFE_COLLATERAL_TOKEN_MARKETS, + collateral_token_address_in: ( + tuple[ChecksumAddress, ...] | None + ) = SAFE_COLLATERAL_TOKEN_MARKETS, + category: str | None = None, ) -> t.List[OmenMarket]: """ Simplified `get_omen_binary_markets` method, which allows to fetch markets based on the filter_by and sort_by values. @@ -363,6 +368,7 @@ def get_omen_binary_markets_simple( created_after=created_after, excluded_questions=excluded_questions, collateral_token_address_in=collateral_token_address_in, + category=category, ) def get_omen_binary_markets( @@ -383,9 +389,11 @@ def get_omen_binary_markets( excluded_questions: set[str] | None = None, # question titles sort_by_field: FieldPath | None = None, sort_direction: str | None = None, - outcomes: list[str] = [OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME], - collateral_token_address_in: tuple[ChecksumAddress, ...] - | None = SAFE_COLLATERAL_TOKEN_MARKETS, + outcomes: list[str] = OMEN_BINARY_MARKET_OUTCOMES, + collateral_token_address_in: ( + tuple[ChecksumAddress, ...] | None + ) = SAFE_COLLATERAL_TOKEN_MARKETS, + category: str | None = None, ) -> t.List[OmenMarket]: """ Complete method to fetch Omen binary markets with various filters, use `get_omen_binary_markets_simple` for simplified version that uses FilterBy and SortBy enums. @@ -406,6 +414,7 @@ def get_omen_binary_markets( excluded_questions=excluded_questions, liquidity_bigger_than=liquidity_bigger_than, collateral_token_address_in=collateral_token_address_in, + category=category, ) # These values can not be set to `None`, but they can be omitted. diff --git a/pyproject.toml b/pyproject.toml index 29af56c1..fa58b24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "prediction-market-agent-tooling" -version = "0.48.14" +version = "0.48.15" description = "Tools to benchmark, deploy and monitor prediction market agents." authors = ["Gnosis"] readme = "README.md" diff --git a/scripts/create_market_omen.py b/scripts/create_market_omen.py index acc2dae2..835edc23 100644 --- a/scripts/create_market_omen.py +++ b/scripts/create_market_omen.py @@ -7,8 +7,7 @@ from prediction_market_agent_tooling.gtypes import private_key_type, xdai_type from prediction_market_agent_tooling.loggers import logger from prediction_market_agent_tooling.markets.omen.data_models import ( - OMEN_FALSE_OUTCOME, - OMEN_TRUE_OUTCOME, + OMEN_BINARY_MARKET_OUTCOMES, ) from prediction_market_agent_tooling.markets.omen.omen import omen_create_market_tx from prediction_market_agent_tooling.markets.omen.omen_contracts import ( @@ -28,7 +27,7 @@ def main( cl_token: CollateralTokenChoice = CollateralTokenChoice.wxdai, fee: float = typer.Option(OMEN_DEFAULT_MARKET_FEE), language: str = typer.Option("en"), - outcomes: list[str] = typer.Option([OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME]), + outcomes: list[str] = typer.Option(OMEN_BINARY_MARKET_OUTCOMES), auto_deposit: bool = typer.Option(False), ) -> None: """ diff --git a/tests/markets/test_betting_strategies.py b/tests/markets/test_betting_strategies.py index 7e22d9ca..f6edbbad 100644 --- a/tests/markets/test_betting_strategies.py +++ b/tests/markets/test_betting_strategies.py @@ -8,10 +8,10 @@ from prediction_market_agent_tooling.gtypes import ( HexBytes, Mana, - OmenOutcomeToken, Probability, Wei, mana_type, + omen_outcome_type, usd_type, wei_type, xdai_type, @@ -64,8 +64,8 @@ def omen_market() -> OmenMarket: ), outcomes=["Yes", "No"], outcomeTokenAmounts=[ - OmenOutcomeToken(7277347438897016099), - OmenOutcomeToken(13741270543921756242), + omen_outcome_type(7277347438897016099), + omen_outcome_type(13741270543921756242), ], outcomeTokenMarginalPrices=[ xdai_type("0.6537666061181695741160552853310822"), diff --git a/tests_integration_with_local_chain/markets/omen/test_omen.py b/tests_integration_with_local_chain/markets/omen/test_omen.py index 36499afa..7a15e9f3 100644 --- a/tests_integration_with_local_chain/markets/omen/test_omen.py +++ b/tests_integration_with_local_chain/markets/omen/test_omen.py @@ -26,6 +26,7 @@ TokenAmount, ) from prediction_market_agent_tooling.markets.omen.data_models import ( + OMEN_BINARY_MARKET_OUTCOMES, OMEN_FALSE_OUTCOME, OMEN_TRUE_OUTCOME, get_bet_outcome, @@ -84,7 +85,7 @@ def test_create_bet_withdraw_resolve_market( closing_time=closing_time, category="cryptocurrency", language="en", - outcomes=[OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME], + outcomes=OMEN_BINARY_MARKET_OUTCOMES, auto_deposit=True, web3=local_web3, ) @@ -147,7 +148,7 @@ def test_omen_create_market_wxdai( closing_time=utcnow() + timedelta(minutes=2), category="cryptocurrency", language="en", - outcomes=[OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME], + outcomes=OMEN_BINARY_MARKET_OUTCOMES, auto_deposit=True, web3=local_web3, ) @@ -170,7 +171,7 @@ def test_omen_create_market_sdai( closing_time=utcnow() + timedelta(minutes=2), category="cryptocurrency", language="en", - outcomes=[OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME], + outcomes=OMEN_BINARY_MARKET_OUTCOMES, auto_deposit=True, collateral_token_address=sDaiContract().address, web3=local_web3,