Skip to content

Commit

Permalink
Refactor omen-specific logic out of DeployableTraderAgent + fix minim…
Browse files Browse the repository at this point in the history
…um required balance to operate (#493)
  • Loading branch information
kongzii authored Oct 21, 2024
1 parent 4dc6fb8 commit 2bf84fc
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 128 deletions.
148 changes: 35 additions & 113 deletions prediction_market_agent_tooling/deploy/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
from enum import Enum
from functools import cached_property

from pydantic import BaseModel, BeforeValidator, computed_field
from pydantic import BeforeValidator, computed_field
from typing_extensions import Annotated
from web3 import Web3
from web3.constants import HASH_ZERO

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.deploy.betting_strategy import (
Expand All @@ -32,11 +30,12 @@
gcp_function_is_active,
gcp_resolve_api_keys_secrets,
)
from prediction_market_agent_tooling.gtypes import HexStr, xDai, xdai_type
from prediction_market_agent_tooling.gtypes import xDai, xdai_type
from prediction_market_agent_tooling.loggers import logger
from prediction_market_agent_tooling.markets.agent_market import (
AgentMarket,
FilterBy,
ProcessedMarket,
SortBy,
)
from prediction_market_agent_tooling.markets.data_models import (
Expand All @@ -49,28 +48,16 @@
MarketType,
have_bet_on_market_since,
)
from prediction_market_agent_tooling.markets.omen.data_models import (
ContractPrediction,
IPFSAgentResult,
)
from prediction_market_agent_tooling.markets.omen.omen import (
is_minimum_required_balance,
redeem_from_all_user_positions,
withdraw_wxdai_to_xdai_to_keep_balance,
)
from prediction_market_agent_tooling.markets.omen.omen_contracts import (
OmenAgentResultMappingContract,
)
from prediction_market_agent_tooling.monitor.monitor_app import (
MARKET_TYPE_TO_DEPLOYED_AGENT,
)
from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
from prediction_market_agent_tooling.tools.ipfs.ipfs_handler import IPFSHandler
from prediction_market_agent_tooling.tools.is_invalid import is_invalid
from prediction_market_agent_tooling.tools.is_predictable import is_predictable_binary
from prediction_market_agent_tooling.tools.langfuse_ import langfuse_context, observe
from prediction_market_agent_tooling.tools.utils import DatetimeUTC, utcnow
from prediction_market_agent_tooling.tools.web3_utils import ipfscidv0_to_byte32

MAX_AVAILABLE_MARKETS = 20
TRADER_TAG = "trader"
Expand Down Expand Up @@ -122,11 +109,6 @@ class OutOfFundsError(ValueError):
pass


class ProcessedMarket(BaseModel):
answer: ProbabilisticAnswer
trades: list[PlacedTrade]


class AnsweredEnum(str, Enum):
ANSWERED = "answered"
NOT_ANSWERED = "not_answered"
Expand Down Expand Up @@ -294,7 +276,6 @@ def get_gcloud_fname(self, market_type: MarketType) -> str:

class DeployableTraderAgent(DeployableAgent):
bet_on_n_markets_per_run: int = 1
min_required_balance_to_operate: xDai | None = xdai_type(1)
min_balance_to_keep_in_native_currency: xDai | None = xdai_type(0.1)
allow_invalid_questions: bool = False
same_market_bet_interval: timedelta = timedelta(hours=24)
Expand Down Expand Up @@ -353,38 +334,25 @@ def update_langfuse_trace_by_processed_market(
]
)

def check_min_required_balance_to_operate(
self,
market_type: MarketType,
check_for_gas: bool = True,
check_for_trades: bool = True,
) -> None:
def check_min_required_balance_to_operate(self, market_type: MarketType) -> None:
api_keys = APIKeys()
if (
market_type == MarketType.OMEN
and check_for_gas
and not is_minimum_required_balance(
api_keys.public_key,
min_required_balance=xdai_type(0.001),
sum_wxdai=False,
)
):

if not market_type.market_class.verify_operational_balance(api_keys):
raise CantPayForGasError(
f"{api_keys.public_key=} doesn't have enough xDai to pay for gas."
f"{api_keys=} doesn't have enough operational balance."
)
if self.min_required_balance_to_operate is None:
return
if (
market_type == MarketType.OMEN
and check_for_trades
and not is_minimum_required_balance(
api_keys.bet_from_address,
min_required_balance=self.min_required_balance_to_operate,
)
):

def check_min_required_balance_to_trade(self, market: AgentMarket) -> None:
api_keys = APIKeys()

# Get the strategy to know how much it will bet.
strategy = self.get_betting_strategy(market)
# Have a little bandwidth after the bet.
min_required_balance_to_trade = strategy.maximum_possible_bet_amount * 1.01

if market.get_trade_balance(api_keys) < min_required_balance_to_trade:
raise OutOfFundsError(
f"Minimum required balance {self.min_required_balance_to_operate} "
f"for agent with address {api_keys.bet_from_address=} is not met."
f"Minimum required balance {min_required_balance_to_trade} for agent is not met."
)

def have_bet_on_market_since(self, market: AgentMarket, since: timedelta) -> bool:
Expand Down Expand Up @@ -447,6 +415,19 @@ def before_process_market(
) -> None:
self.update_langfuse_trace_by_market(market_type, market)

api_keys = APIKeys()

self.check_min_required_balance_to_trade(market)

if market_type.is_blockchain_market:
# Exchange wxdai back to xdai if the balance is getting low, so we can keep paying for fees.
if self.min_balance_to_keep_in_native_currency is not None:
withdraw_wxdai_to_xdai_to_keep_balance(
api_keys,
min_required_balance=self.min_balance_to_keep_in_native_currency,
withdraw_multiplier=2,
)

def process_market(
self,
market_type: MarketType,
Expand Down Expand Up @@ -510,72 +491,16 @@ def after_process_market(
market: AgentMarket,
processed_market: ProcessedMarket,
) -> None:
if market_type != MarketType.OMEN:
logger.info(
f"Skipping after_process_market since market_type {market_type} != OMEN"
)
return
keys = APIKeys()
self.store_prediction(
market_id=market.id, processed_market=processed_market, keys=keys
)

def store_prediction(
self, market_id: str, processed_market: ProcessedMarket, keys: APIKeys
) -> None:
reasoning = (
processed_market.answer.reasoning
if processed_market.answer.reasoning
else ""
)

ipfs_hash_decoded = HexBytes(HASH_ZERO)
if keys.enable_ipfs_upload:
logger.info("Storing prediction on IPFS.")
ipfs_hash = IPFSHandler(keys).store_agent_result(
IPFSAgentResult(reasoning=reasoning)
)
ipfs_hash_decoded = ipfscidv0_to_byte32(ipfs_hash)

tx_hashes = [
HexBytes(HexStr(i.id)) for i in processed_market.trades if i.id is not None
]
prediction = ContractPrediction(
publisher=keys.public_key,
ipfs_hash=ipfs_hash_decoded,
tx_hashes=tx_hashes,
estimated_probability_bps=int(processed_market.answer.p_yes * 10000),
)
tx_receipt = OmenAgentResultMappingContract().add_prediction(
api_keys=keys,
market_address=Web3.to_checksum_address(market_id),
prediction=prediction,
)
logger.info(
f"Added prediction to market {market_id}. - receipt {tx_receipt['transactionHash'].hex()}."
)
market.store_prediction(processed_market=processed_market, keys=keys)

def before_process_markets(self, market_type: MarketType) -> None:
"""
Executes actions that occur before bets are placed.
"""
api_keys = APIKeys()
if market_type == MarketType.OMEN:
# First, check if we have enough xDai to pay for gas, there is no way of doing anything without it.
self.check_min_required_balance_to_operate(
market_type, check_for_trades=False
)
# Omen is specific, because the user (agent) needs to manually withdraw winnings from the market.
redeem_from_all_user_positions(api_keys)
# After redeeming, check if we have enough xDai to pay for gas and place bets.
self.check_min_required_balance_to_operate(market_type)
# Exchange wxdai back to xdai if the balance is getting low, so we can keep paying for fees.
if self.min_balance_to_keep_in_native_currency is not None:
withdraw_wxdai_to_xdai_to_keep_balance(
api_keys,
min_required_balance=self.min_balance_to_keep_in_native_currency,
withdraw_multiplier=2,
)
self.check_min_required_balance_to_operate(market_type)
market_type.market_class.redeem_winnings(api_keys)

def process_markets(self, market_type: MarketType) -> None:
"""
Expand All @@ -589,9 +514,6 @@ def process_markets(self, market_type: MarketType) -> None:
processed = 0

for market in available_markets:
# We need to check it again before each market bet, as the balance might have changed.
self.check_min_required_balance_to_operate(market_type)

processed_market = self.process_market(market_type, market)

if processed_market is not None:
Expand All @@ -603,7 +525,7 @@ def process_markets(self, market_type: MarketType) -> None:
logger.info("All markets processed.")

def after_process_markets(self, market_type: MarketType) -> None:
pass
"Executes actions that occur after bets are placed."

def run(self, market_type: MarketType) -> None:
self.before_process_markets(market_type)
Expand Down
19 changes: 18 additions & 1 deletion prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ def calculate_trades(
answer: ProbabilisticAnswer,
market: AgentMarket,
) -> list[Trade]:
pass
raise NotImplementedError("Subclass should implement this.")

@property
@abstractmethod
def maximum_possible_bet_amount(self) -> float:
raise NotImplementedError("Subclass should implement this.")

def build_zero_token_amount(self, currency: Currency) -> TokenAmount:
return TokenAmount(amount=0, currency=currency)
Expand Down Expand Up @@ -126,6 +131,10 @@ class MaxAccuracyBettingStrategy(BettingStrategy):
def __init__(self, bet_amount: float):
self.bet_amount = bet_amount

@property
def maximum_possible_bet_amount(self) -> float:
return self.bet_amount

def calculate_trades(
self,
existing_position: Position | None,
Expand Down Expand Up @@ -168,6 +177,10 @@ def __init__(self, max_bet_amount: float, max_price_impact: float | None = None)
self.max_bet_amount = max_bet_amount
self.max_price_impact = max_price_impact

@property
def maximum_possible_bet_amount(self) -> float:
return self.max_bet_amount

def calculate_trades(
self,
existing_position: Position | None,
Expand Down Expand Up @@ -282,6 +295,10 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
def __init__(self, max_bet_amount: float = 10):
self.max_bet_amount = max_bet_amount

@property
def maximum_possible_bet_amount(self) -> float:
return self.max_bet_amount

def adjust_bet_amount(
self, existing_position: Position | None, market: AgentMarket
) -> float:
Expand Down
37 changes: 37 additions & 0 deletions prediction_market_agent_tooling/markets/agent_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
Bet,
BetAmount,
Currency,
PlacedTrade,
Position,
ProbabilisticAnswer,
Resolution,
ResolvedBet,
TokenAmount,
Expand All @@ -25,6 +27,11 @@
)


class ProcessedMarket(BaseModel):
answer: ProbabilisticAnswer
trades: list[PlacedTrade]


class SortBy(str, Enum):
CLOSING_SOONEST = "closing-soonest"
NEWEST = "newest"
Expand Down Expand Up @@ -198,6 +205,36 @@ def get_binary_markets(
def get_binary_market(id: str) -> "AgentMarket":
raise NotImplementedError("Subclasses must implement this method")

@staticmethod
def redeem_winnings(api_keys: APIKeys) -> None:
"""
On some markets (like Omen), it's needed to manually claim the winner bets. If it's not needed, just implement with `pass`.
"""
raise NotImplementedError("Subclasses must implement this method")

@staticmethod
def get_trade_balance(api_keys: APIKeys) -> float:
"""
Return balance that can be used to trade on the given market.
"""
raise NotImplementedError("Subclasses must implement this method")

@staticmethod
def verify_operational_balance(api_keys: APIKeys) -> bool:
"""
Return `True` if the user has enough of operational balance. If not needed, just return `True`.
For example: Omen needs at least some xDai in the wallet to execute transactions.
"""
raise NotImplementedError("Subclasses must implement this method")

def store_prediction(
self, processed_market: ProcessedMarket, keys: APIKeys
) -> None:
"""
If market allows to upload predictions somewhere, implement it in this method.
"""
raise NotImplementedError("Subclasses must implement this method")

@staticmethod
def get_bets_made_since(
better_address: ChecksumAddress, start_time: DatetimeUTC
Expand Down
5 changes: 5 additions & 0 deletions prediction_market_agent_tooling/markets/manifold/manifold.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ def get_binary_markets(
)
]

@staticmethod
def redeem_winnings(api_keys: APIKeys) -> None:
# It's done automatically on Manifold.
pass

@classmethod
def get_user_url(cls, keys: APIKeys) -> str:
return get_authenticated_user(keys.manifold_api_key.get_secret_value()).url
Expand Down
4 changes: 4 additions & 0 deletions prediction_market_agent_tooling/markets/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def market_class(self) -> type[AgentMarket]:
raise ValueError(f"Unknown market type: {self}")
return MARKET_TYPE_TO_AGENT_MARKET[self]

@property
def is_blockchain_market(self) -> bool:
return self in [MarketType.OMEN, MarketType.POLYMARKET]


MARKET_TYPE_TO_AGENT_MARKET: dict[MarketType, type[AgentMarket]] = {
MarketType.MANIFOLD: ManifoldAgentMarket,
Expand Down
Loading

0 comments on commit 2bf84fc

Please sign in to comment.