diff --git a/.gitignore b/.gitignore index 891d254c..0c756e47 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,5 @@ cython_debug/ tests_files/* !tests_files/.gitkeep + +bet_strategy_benchmark* diff --git a/examples/monitor/match_bets_with_langfuse_traces.py b/examples/monitor/match_bets_with_langfuse_traces.py index ded3b8a0..11f09244 100644 --- a/examples/monitor/match_bets_with_langfuse_traces.py +++ b/examples/monitor/match_bets_with_langfuse_traces.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Any import pandas as pd @@ -89,6 +90,9 @@ def get_outcome_for_trace( if __name__ == "__main__": + output_directory = Path("bet_strategy_benchmark") + output_directory.mkdir(parents=True, exist_ok=True) + # Get the private keys for the agents from GCP Secret Manager agent_gcp_secret_map = { "DeployablePredictionProphetGPT4TurboFinalAgent": "pma-prophetgpt4turbo-final", @@ -240,7 +244,8 @@ def get_outcome_for_trace( details.sort(key=lambda x: x["sim_profit"], reverse=True) pd.DataFrame.from_records(details).to_csv( - f"{agent_name} - {strategy} - all bets.csv", index=False + output_directory / f"{agent_name} - {strategy} - all bets.csv", + index=False, ) sum_squared_errors = 0.0 @@ -297,7 +302,9 @@ def get_outcome_for_trace( + simulations_df.to_markdown(index=False) ) # export details per agent - pd.DataFrame.from_records(details).to_csv(f"{agent_name}_details.csv") + pd.DataFrame.from_records(details).to_csv( + output_directory / f"{agent_name}_details.csv" + ) print(f"Correlation between p_yes mse and total profit:") for strategy_name, mse_profit in strat_mse_profits.items(): @@ -306,5 +313,7 @@ def get_outcome_for_trace( correlation = pd.Series(mse).corr(pd.Series(profit)) print(f"{strategy_name}: {correlation=}") - with open("match_bets_with_langfuse_traces_overall.md", "w") as overall_f: + with open( + output_directory / "match_bets_with_langfuse_traces_overall.md", "w" + ) as overall_f: overall_f.write(overall_md) diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index 52259628..07b04220 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -4,7 +4,7 @@ from prediction_market_agent_tooling.gtypes import xDai from prediction_market_agent_tooling.loggers import logger -from prediction_market_agent_tooling.markets.agent_market import AgentMarket +from prediction_market_agent_tooling.markets.agent_market import AgentMarket, MarketFees from prediction_market_agent_tooling.markets.data_models import ( Currency, Position, @@ -19,7 +19,6 @@ ) from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import ( get_kelly_bet_full, - get_kelly_bet_simplified, ) from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet from prediction_market_agent_tooling.tools.utils import check_not_none @@ -153,32 +152,24 @@ def calculate_trades( market: AgentMarket, ) -> list[Trade]: outcome_token_pool = check_not_none(market.outcome_token_pool) - kelly_bet = ( - get_kelly_bet_full( - yes_outcome_pool_size=outcome_token_pool[ - market.get_outcome_str_from_bool(True) - ], - no_outcome_pool_size=outcome_token_pool[ - market.get_outcome_str_from_bool(False) - ], - estimated_p_yes=answer.p_yes, - max_bet=self.max_bet_amount, - confidence=answer.confidence, - ) - if market.has_token_pool() - else get_kelly_bet_simplified( - self.max_bet_amount, - market.current_p_yes, - answer.p_yes, - answer.confidence, - ) + kelly_bet = get_kelly_bet_full( + yes_outcome_pool_size=outcome_token_pool[ + market.get_outcome_str_from_bool(True) + ], + no_outcome_pool_size=outcome_token_pool[ + market.get_outcome_str_from_bool(False) + ], + estimated_p_yes=answer.p_yes, + max_bet=self.max_bet_amount, + confidence=answer.confidence, + fees=market.fees, ) kelly_bet_size = kelly_bet.size if self.max_price_impact: # Adjust amount max_price_impact_bet_amount = self.calculate_bet_amount_for_price_impact( - market, kelly_bet, 0 + market, kelly_bet ) # We just don't want Kelly size to extrapolate price_impact - hence we take the min. @@ -196,7 +187,12 @@ def calculate_trades( return trades def calculate_price_impact_for_bet_amount( - self, buy_direction: bool, bet_amount: float, yes: float, no: float, fee: float + self, + buy_direction: bool, + bet_amount: float, + yes: float, + no: float, + fees: MarketFees, ) -> float: total_outcome_tokens = yes + no expected_price = ( @@ -204,7 +200,7 @@ def calculate_price_impact_for_bet_amount( ) tokens_to_buy = get_buy_outcome_token_amount( - bet_amount, buy_direction, yes, no, fee + bet_amount, buy_direction, yes, no, fees ) actual_price = bet_amount / tokens_to_buy @@ -216,7 +212,6 @@ def calculate_bet_amount_for_price_impact( self, market: AgentMarket, kelly_bet: SimpleBet, - fee: float, ) -> float: def calculate_price_impact_deviation_from_target_price_impact( bet_amount: xDai, @@ -226,7 +221,7 @@ def calculate_price_impact_deviation_from_target_price_impact( bet_amount, yes_outcome_pool_size, no_outcome_pool_size, - fee, + market.fees, ) # We return abs for the algorithm to converge to 0 instead of the min (and possibly negative) value. @@ -285,25 +280,17 @@ def calculate_trades( estimated_p_yes = float(answer.p_yes > 0.5) confidence = 1.0 - kelly_bet = ( - get_kelly_bet_full( - yes_outcome_pool_size=outcome_token_pool[ - market.get_outcome_str_from_bool(True) - ], - no_outcome_pool_size=outcome_token_pool[ - market.get_outcome_str_from_bool(False) - ], - estimated_p_yes=estimated_p_yes, - max_bet=adjusted_bet_amount, - confidence=confidence, - ) - if market.has_token_pool() - else get_kelly_bet_simplified( - adjusted_bet_amount, - market.current_p_yes, - estimated_p_yes, - confidence, - ) + kelly_bet = get_kelly_bet_full( + yes_outcome_pool_size=outcome_token_pool[ + market.get_outcome_str_from_bool(True) + ], + no_outcome_pool_size=outcome_token_pool[ + market.get_outcome_str_from_bool(False) + ], + estimated_p_yes=estimated_p_yes, + max_bet=adjusted_bet_amount, + confidence=confidence, + fees=market.fees, ) amounts = { diff --git a/prediction_market_agent_tooling/jobs/omen/omen_jobs.py b/prediction_market_agent_tooling/jobs/omen/omen_jobs.py index ea80e20a..94e868ce 100644 --- a/prediction_market_agent_tooling/jobs/omen/omen_jobs.py +++ b/prediction_market_agent_tooling/jobs/omen/omen_jobs.py @@ -75,7 +75,7 @@ def from_omen_agent_market(market: OmenAgentMarket) -> "OmenJobAgentMarket": market_maker_contract_address_checksummed=market.market_maker_contract_address_checksummed, condition=market.condition, finalized_time=market.finalized_time, - fee=market.fee, + fees=market.fees, ) diff --git a/prediction_market_agent_tooling/markets/agent_market.py b/prediction_market_agent_tooling/markets/agent_market.py index 358f8486..ccd1fed7 100644 --- a/prediction_market_agent_tooling/markets/agent_market.py +++ b/prediction_market_agent_tooling/markets/agent_market.py @@ -2,7 +2,7 @@ from enum import Enum from eth_typing import ChecksumAddress -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, field_validator, model_validator from pydantic_core.core_schema import FieldValidationInfo from prediction_market_agent_tooling.config import APIKeys @@ -16,6 +16,7 @@ ResolvedBet, TokenAmount, ) +from prediction_market_agent_tooling.markets.market_fees import MarketFees from prediction_market_agent_tooling.tools.utils import ( DatetimeUTC, check_not_none, @@ -60,6 +61,7 @@ class AgentMarket(BaseModel): current_p_yes: Probability url: str volume: float | None # Should be in currency of `currency` above. + fees: MarketFees @field_validator("outcome_token_pool") def validate_outcome_token_pool( @@ -77,6 +79,14 @@ def validate_outcome_token_pool( ) return outcome_token_pool + @model_validator(mode="before") + def handle_legacy_fee(cls, data: dict[str, t.Any]) -> dict[str, t.Any]: + # Backward compatibility for older `AgentMarket` without `fees`. + if "fees" not in data and "fee" in data: + data["fees"] = MarketFees(absolute=0.0, bet_proportion=data["fee"]) + del data["fee"] + return data + @property def current_p_no(self) -> Probability: return Probability(1 - self.current_p_yes) diff --git a/prediction_market_agent_tooling/markets/manifold/manifold.py b/prediction_market_agent_tooling/markets/manifold/manifold.py index 7cbc2c00..53cafbb7 100644 --- a/prediction_market_agent_tooling/markets/manifold/manifold.py +++ b/prediction_market_agent_tooling/markets/manifold/manifold.py @@ -6,6 +6,7 @@ from prediction_market_agent_tooling.markets.agent_market import ( AgentMarket, FilterBy, + MarketFees, SortBy, ) from prediction_market_agent_tooling.markets.data_models import BetAmount, Currency @@ -33,6 +34,13 @@ class ManifoldAgentMarket(AgentMarket): currency: t.ClassVar[Currency] = Currency.Mana base_url: t.ClassVar[str] = MANIFOLD_BASE_URL + # Manifold has additional fees than `platform_absolute`, but they don't expose them in the API before placing the bet, see https://docs.manifold.markets/api. + # So we just consider them as 0, which anyway is true for all markets I randomly checked on Manifold. + fees: MarketFees = MarketFees( + bet_proportion=0, + absolute=0.25, # For doing trades via API. + ) + def get_last_trade_p_yes(self) -> Probability: """On Manifold, probablities aren't updated after the closure, so we can just use the current probability""" return self.current_p_yes diff --git a/prediction_market_agent_tooling/markets/market_fees.py b/prediction_market_agent_tooling/markets/market_fees.py new file mode 100644 index 00000000..aefb8b25 --- /dev/null +++ b/prediction_market_agent_tooling/markets/market_fees.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field + + +class MarketFees(BaseModel): + bet_proportion: float = Field( + ..., ge=0.0, lt=1.0 + ) # proportion of the bet, from 0 to 1 + absolute: float # absolute value paid in the currency of the market + + @staticmethod + def get_zero_fees( + bet_proportion: float = 0.0, + absolute: float = 0.0, + ) -> "MarketFees": + return MarketFees( + bet_proportion=bet_proportion, + absolute=absolute, + ) + + def total_fee_absolute_value(self, bet_amount: float) -> float: + """ + Returns the total fee in absolute terms, including both proportional and fixed fees. + """ + return self.bet_proportion * bet_amount + self.absolute + + def total_fee_relative_value(self, bet_amount: float) -> float: + """ + Returns the total fee relative to the bet amount, including both proportional and fixed fees. + """ + if bet_amount == 0: + return 0.0 + total_fee = self.total_fee_absolute_value(bet_amount) + return total_fee / bet_amount + + def get_bet_size_after_fees(self, bet_amount: float) -> float: + return bet_amount * (1 - self.bet_proportion) - self.absolute diff --git a/prediction_market_agent_tooling/markets/metaculus/metaculus.py b/prediction_market_agent_tooling/markets/metaculus/metaculus.py index e9d9a518..fe52e4ef 100644 --- a/prediction_market_agent_tooling/markets/metaculus/metaculus.py +++ b/prediction_market_agent_tooling/markets/metaculus/metaculus.py @@ -5,6 +5,7 @@ from prediction_market_agent_tooling.markets.agent_market import ( AgentMarket, FilterBy, + MarketFees, SortBy, ) from prediction_market_agent_tooling.markets.metaculus.api import ( @@ -29,6 +30,7 @@ class MetaculusAgentMarket(AgentMarket): description: str | None = ( None # Metaculus markets don't have a description, so just default to None. ) + fees: MarketFees = MarketFees.get_zero_fees() # No fees on Metaculus. @staticmethod def from_data_model(model: MetaculusQuestion) -> "MetaculusAgentMarket": diff --git a/prediction_market_agent_tooling/markets/omen/omen.py b/prediction_market_agent_tooling/markets/omen/omen.py index d1b88343..28879fa3 100644 --- a/prediction_market_agent_tooling/markets/omen/omen.py +++ b/prediction_market_agent_tooling/markets/omen/omen.py @@ -22,6 +22,7 @@ from prediction_market_agent_tooling.markets.agent_market import ( AgentMarket, FilterBy, + MarketFees, SortBy, ) from prediction_market_agent_tooling.markets.data_models import ( @@ -101,7 +102,6 @@ class OmenAgentMarket(AgentMarket): finalized_time: DatetimeUTC | None created_time: DatetimeUTC close_time: DatetimeUTC - fee: float # proportion, from 0 to 1 _binary_market_p_yes_history: list[Probability] | None = None description: str | None = ( @@ -240,7 +240,7 @@ def calculate_sell_amount_in_collateral( shares_to_sell=amount.amount, holdings=wei_to_xdai(pool_balance[self.get_index_set(sell_str)]), other_holdings=wei_to_xdai(pool_balance[self.get_index_set(other_str)]), - fee=self.fee, + fees=self.fees, ) return xDai(collateral) @@ -352,7 +352,12 @@ def from_data_model(model: OmenMarket) -> "OmenAgentMarket": url=model.url, volume=wei_to_xdai(model.collateralVolume), close_time=model.close_time, - fee=float(wei_to_xdai(model.fee)) if model.fee is not None else 0.0, + fees=MarketFees( + bet_proportion=( + float(wei_to_xdai(model.fee)) if model.fee is not None else 0.0 + ), + absolute=0, + ), outcome_token_pool={ model.outcomes[i]: wei_to_xdai(Wei(model.outcomeTokenAmounts[i])) for i in range(len(model.outcomes)) @@ -598,7 +603,7 @@ def get_buy_token_amount( buy_direction=direction, yes_outcome_pool_size=outcome_token_pool[OMEN_TRUE_OUTCOME], no_outcome_pool_size=outcome_token_pool[OMEN_FALSE_OUTCOME], - fee=self.fee, + fees=self.fees, ) return TokenAmount(amount=amount, currency=self.currency) @@ -628,10 +633,10 @@ def get_new_p_yes(self, bet_amount: BetAmount, direction: bool) -> Probability: no_outcome_pool_size = outcome_token_pool[self.get_outcome_str_from_bool(False)] new_yes_outcome_pool_size = yes_outcome_pool_size + ( - bet_amount.amount * (1 - self.fee) + self.fees.get_bet_size_after_fees(bet_amount.amount) ) new_no_outcome_pool_size = no_outcome_pool_size + ( - bet_amount.amount * (1 - self.fee) + self.fees.get_bet_size_after_fees(bet_amount.amount) ) received_token_amount = self.get_buy_token_amount(bet_amount, direction).amount @@ -1104,7 +1109,6 @@ def omen_remove_fund_market_tx( market_contract.removeFunding(api_keys=api_keys, remove_funding=shares, web3=web3) conditional_tokens = OmenConditionalTokenContract() - parent_collection_id = build_parent_collection_id() amount_per_index_set = get_conditional_tokens_balance_for_market( market, from_address, web3 ) @@ -1116,7 +1120,6 @@ def omen_remove_fund_market_tx( result = conditional_tokens.mergePositions( api_keys=api_keys, collateral_token_address=market.collateral_token_contract_address_checksummed, - parent_collection_id=parent_collection_id, conditionId=market.condition.id, index_sets=market.condition.index_sets, amount=amount_to_merge, @@ -1266,14 +1269,14 @@ def get_buy_outcome_token_amount( buy_direction: bool, yes_outcome_pool_size: float, no_outcome_pool_size: float, - fee: float, + fees: MarketFees, ) -> float: """ Calculates the amount of outcome tokens received for a given investment Taken from https://github.com/gnosis/conditional-tokens-market-makers/blob/6814c0247c745680bb13298d4f0dd7f5b574d0db/contracts/FixedProductMarketMaker.sol#L264 """ - investment_amount_minus_fees = investment_amount * (1 - fee) + investment_amount_minus_fees = fees.get_bet_size_after_fees(investment_amount) buy_token_pool_balance = ( yes_outcome_pool_size if buy_direction else no_outcome_pool_size ) diff --git a/prediction_market_agent_tooling/markets/omen/omen_contracts.py b/prediction_market_agent_tooling/markets/omen/omen_contracts.py index 02c8c3d1..1a1f0db2 100644 --- a/prediction_market_agent_tooling/markets/omen/omen_contracts.py +++ b/prediction_market_agent_tooling/markets/omen/omen_contracts.py @@ -163,10 +163,10 @@ def mergePositions( self, api_keys: APIKeys, collateral_token_address: ChecksumAddress, - parent_collection_id: HexStr, conditionId: HexBytes, index_sets: t.List[int], amount: Wei, + parent_collection_id: HexStr = build_parent_collection_id(), web3: Web3 | None = None, ) -> TxReceipt: return self.send( diff --git a/prediction_market_agent_tooling/markets/polymarket/polymarket.py b/prediction_market_agent_tooling/markets/polymarket/polymarket.py index 8e0b5bcb..c85b254f 100644 --- a/prediction_market_agent_tooling/markets/polymarket/polymarket.py +++ b/prediction_market_agent_tooling/markets/polymarket/polymarket.py @@ -3,6 +3,7 @@ from prediction_market_agent_tooling.markets.agent_market import ( AgentMarket, FilterBy, + MarketFees, SortBy, ) from prediction_market_agent_tooling.markets.data_models import BetAmount, Currency @@ -26,6 +27,12 @@ class PolymarketAgentMarket(AgentMarket): currency: t.ClassVar[Currency] = Currency.USDC base_url: t.ClassVar[str] = POLYMARKET_BASE_URL + # Based on https://docs.polymarket.com/#fees, there are currently no fees, except for transactions fees. + # However they do have `maker_fee_base_rate` and `taker_fee_base_rate`, but impossible to test out our implementation without them actually taking the fees. + # But then in the new subgraph API, they have `fee: BigInt! (Percentage fee of trades taken by market maker. A 2% fee is represented as 2*10^16)`. + # TODO: Check out the fees while integrating the subgraph API or if we implement placing of bets on Polymarket. + fees: MarketFees = MarketFees.get_zero_fees() + @staticmethod def from_data_model(model: PolymarketMarketWithPrices) -> "PolymarketAgentMarket": return PolymarketAgentMarket( diff --git a/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py b/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py index 4c762251..c7ff990d 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py @@ -1,3 +1,4 @@ +from prediction_market_agent_tooling.markets.market_fees import MarketFees from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet @@ -61,7 +62,7 @@ def get_kelly_bet_full( estimated_p_yes: float, confidence: float, max_bet: float, - fee: float = 0.0, # proportion, 0 to 1 + fees: MarketFees, ) -> SimpleBet: """ Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market. @@ -86,9 +87,14 @@ def get_kelly_bet_full( limitations under the License. ``` """ + fee = fees.bet_proportion + if fees.absolute > 0: + raise RuntimeError( + f"Kelly works only with bet-proportional fees, but the fees are {fees=}." + ) + check_is_valid_probability(estimated_p_yes) check_is_valid_probability(confidence) - check_is_valid_probability(fee) if max_bet == 0: return SimpleBet(direction=True, size=0) diff --git a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py index 32446ff2..996f7d95 100644 --- a/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +++ b/prediction_market_agent_tooling/tools/betting_strategies/market_moving.py @@ -3,7 +3,10 @@ import numpy as np from prediction_market_agent_tooling.gtypes import Probability, Wei, xDai -from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket +from prediction_market_agent_tooling.markets.omen.omen import ( + MarketFees, + OmenAgentMarket, +) from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet from prediction_market_agent_tooling.tools.utils import check_not_none from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai, xdai_to_wei @@ -14,7 +17,7 @@ def get_market_moving_bet( no_outcome_pool_size: float, market_p_yes: float, target_p_yes: float, - fee: float = 0.0, # proportion, 0 to 1 + fees: MarketFees, max_iters: int = 100, ) -> SimpleBet: """ @@ -47,7 +50,7 @@ def get_market_moving_bet( # Binary search for the optimal bet amount for _ in range(max_iters): bet_amount = (min_bet_amount + max_bet_amount) / 2 - amounts_diff = bet_amount * (1 - fee) + amounts_diff = fees.get_bet_size_after_fees(bet_amount) # Initial new amounts are old amounts + equal new amounts for each outcome yes_outcome_new_pool_size = yes_outcome_pool_size + amounts_diff @@ -106,16 +109,20 @@ def _sanity_check_omen_market_moving_bet( no_outcome_pool_size = outcome_token_pool[market.get_outcome_str_from_bool(False)] market_const = yes_outcome_pool_size * no_outcome_pool_size + bet_to_check_size_after_fees = market.fees.get_bet_size_after_fees( + bet_to_check.size + ) + # When you buy 'yes' tokens, you add your bet size to the both pools, then # subtract `buy_amount` from the 'yes' pool. And vice versa for 'no' tokens. new_yes_outcome_pool_size = ( yes_outcome_pool_size - + (bet_to_check.size * (1 - market.fee)) + + bet_to_check_size_after_fees - float(bet_to_check.direction) * buy_amount ) new_no_outcome_pool_size = ( no_outcome_pool_size - + (bet_to_check.size * (1 - market.fee)) + + bet_to_check_size_after_fees - float(not bet_to_check.direction) * buy_amount ) new_market_const = new_yes_outcome_pool_size * new_no_outcome_pool_size diff --git a/prediction_market_agent_tooling/tools/langfuse_client_utils.py b/prediction_market_agent_tooling/tools/langfuse_client_utils.py index 4452662b..9c052800 100644 --- a/prediction_market_agent_tooling/tools/langfuse_client_utils.py +++ b/prediction_market_agent_tooling/tools/langfuse_client_utils.py @@ -93,15 +93,18 @@ def get_traces_for_agent( def trace_to_omen_agent_market(trace: TraceWithDetails) -> OmenAgentMarket | None: if not trace.input: + logger.warning(f"No input in the trace: {trace}") return None if not trace.input["args"]: + logger.warning(f"No args in the trace: {trace}") return None assert len(trace.input["args"]) == 2 and trace.input["args"][0] == "omen" try: # If the market model is invalid (e.g. outdated), it will raise an exception market = OmenAgentMarket.model_validate(trace.input["args"][1]) return market - except Exception: + except Exception as e: + logger.warning(f"Market not parsed from langfuse because: {e}") return None diff --git a/prediction_market_agent_tooling/tools/utils.py b/prediction_market_agent_tooling/tools/utils.py index 42ac1a03..d6b94b18 100644 --- a/prediction_market_agent_tooling/tools/utils.py +++ b/prediction_market_agent_tooling/tools/utils.py @@ -18,6 +18,7 @@ SecretStr, ) from prediction_market_agent_tooling.loggers import logger +from prediction_market_agent_tooling.markets.market_fees import MarketFees T = TypeVar("T") @@ -189,7 +190,7 @@ def calculate_sell_amount_in_collateral( shares_to_sell: float, holdings: float, other_holdings: float, - fee: float, + fees: MarketFees, ) -> float: """ Computes the amount of collateral that needs to be sold to get `shares` @@ -198,16 +199,12 @@ def calculate_sell_amount_in_collateral( 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) + R = (r + fees.absolute) / (1 - fees.bet_proportion) first_term = other_holdings - R second_term = holdings + shares_to_sell - R third_term = holdings * other_holdings diff --git a/scripts/create_market_omen.py b/scripts/create_market_omen.py index c575fca6..f6d42f2f 100644 --- a/scripts/create_market_omen.py +++ b/scripts/create_market_omen.py @@ -25,7 +25,7 @@ def main( initial_funds: str = typer.Option(), from_private_key: str = typer.Option(), safe_address: str = typer.Option(None), - cl_token: CollateralTokenChoice = CollateralTokenChoice.wxdai, + cl_token: CollateralTokenChoice = CollateralTokenChoice.sdai, fee_perc: float = typer.Option(OMEN_DEFAULT_MARKET_FEE_PERC), language: str = typer.Option("en"), outcomes: list[str] = typer.Option(OMEN_BINARY_MARKET_OUTCOMES), @@ -42,8 +42,6 @@ def main( --initial-funds 0.01 \ --from-private-key your-private-key ``` - - Market can be created also on the web: https://aiomen.eth.limo/#/create """ safe_address_checksum = ( Web3.to_checksum_address(safe_address) if safe_address else None @@ -52,7 +50,7 @@ def main( BET_FROM_PRIVATE_KEY=private_key_type(from_private_key), SAFE_ADDRESS=safe_address_checksum, ) - market_address = omen_create_market_tx( + market = omen_create_market_tx( api_keys=api_keys, collateral_token_address=COLLATERAL_TOKEN_CHOICE_TO_ADDRESS[cl_token], initial_funds=xdai_type(initial_funds), @@ -64,7 +62,7 @@ def main( outcomes=outcomes, auto_deposit=auto_deposit, ) - logger.info(f"Market created at address: {market_address}") + logger.info(f"Market created: {market}") if __name__ == "__main__": diff --git a/scripts/remove_liquidity_omen.py b/scripts/remove_liquidity_omen.py new file mode 100644 index 00000000..d0c42060 --- /dev/null +++ b/scripts/remove_liquidity_omen.py @@ -0,0 +1,47 @@ +import typer +from web3 import Web3 + +from prediction_market_agent_tooling.config import APIKeys +from prediction_market_agent_tooling.gtypes import private_key_type +from prediction_market_agent_tooling.loggers import logger +from prediction_market_agent_tooling.markets.omen.omen import ( + OmenAgentMarket, + omen_remove_fund_market_tx, +) +from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import ( + OmenSubgraphHandler, +) + + +def main( + market_id: str = typer.Option(), + from_private_key: str = typer.Option(), + safe_address: str = typer.Option(None), +) -> None: + """ + Helper script to remove your liquidity from a market on Omen, usage: + + ```bash + python scripts/remove_liquidity_omen.py \ + --market-id "0x176122ecc05d3b1364fa815f4e01ddad8a2a66bc" \ + --from-private-key your-private-key + ``` + """ + safe_address_checksum = ( + Web3.to_checksum_address(safe_address) if safe_address else None + ) + api_keys = APIKeys( + BET_FROM_PRIVATE_KEY=private_key_type(from_private_key), + SAFE_ADDRESS=safe_address_checksum, + ) + market = OmenAgentMarket.from_data_model( + OmenSubgraphHandler().get_omen_market_by_market_id( + Web3.to_checksum_address(market_id) + ) + ) + omen_remove_fund_market_tx(api_keys=api_keys, market=market, shares=None) + logger.info(f"Liquidity removed from: {market_id}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/markets/omen/test_omen.py b/tests/markets/omen/test_omen.py index e61904b9..f3b3c8bb 100644 --- a/tests/markets/omen/test_omen.py +++ b/tests/markets/omen/test_omen.py @@ -242,7 +242,7 @@ def test_get_new_p_yes() -> None: no_outcome_pool_size=no_outcome_pool_size, market_p_yes=market.current_p_yes, target_p_yes=0.95, - fee=market.fee, + fees=market.fees, ) new_p_yes = market.get_new_p_yes( bet_amount=market.get_bet_amount(bet.size), direction=bet.direction diff --git a/tests/markets/test_betting_strategies.py b/tests/markets/test_betting_strategies.py index 4353d98e..de5dd7d1 100644 --- a/tests/markets/test_betting_strategies.py +++ b/tests/markets/test_betting_strategies.py @@ -25,7 +25,10 @@ OmenMarket, Question, ) -from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket +from prediction_market_agent_tooling.markets.omen.omen import ( + MarketFees, + OmenAgentMarket, +) from prediction_market_agent_tooling.markets.omen.omen_contracts import ( WrappedxDaiContract, ) @@ -119,7 +122,7 @@ def test_minimum_bet_to_win( url="url", volume=None, finalized_time=None, - fee=0.02, + fees=MarketFees.get_zero_fees(bet_proportion=0.02), outcome_token_pool=None, ), ) @@ -180,7 +183,7 @@ def test_get_market_moving_bet( no_outcome_pool_size=wei_to_xdai( Wei(omen_market.outcomeTokenAmounts[omen_market.no_index]) ), - fee=wei_to_xdai(check_not_none(omen_market.fee)), + fees=OmenAgentMarket.from_data_model(omen_market).fees, ) assert np.isclose( bet.size, @@ -207,7 +210,7 @@ def test_sanity_check_market_moving_bet(target_p_yes: float) -> None: no_outcome_pool_size=no_outcome_pool_size, market_p_yes=market.current_p_yes, target_p_yes=target_p_yes, - fee=market.fee, + fees=market.fees, ) _sanity_check_omen_market_moving_bet(market_moving_bet, market, target_p_yes) @@ -258,6 +261,7 @@ def test_kelly_bet(est_p_yes: Probability, omen_market: OmenMarket) -> None: estimated_p_yes=est_p_yes, max_bet=max_bet, confidence=confidence, + fees=MarketFees.get_zero_fees(), ).direction == expected_bet_direction ) @@ -279,7 +283,7 @@ def test_zero_bets() -> None: no_outcome_pool_size=no_outcome_pool_size, market_p_yes=market.current_p_yes, target_p_yes=market.current_p_yes, - fee=market.fee, + fees=market.fees, ) assert np.isclose(market_moving_bet.size, 0.0, atol=1e-3) @@ -289,7 +293,7 @@ def test_zero_bets() -> None: estimated_p_yes=market.current_p_yes, confidence=1.0, max_bet=0, - fee=market.fee, + fees=market.fees, ) assert kelly_bet.size == 0 diff --git a/tests/markets/test_markets.py b/tests/markets/test_markets.py index 255b246f..89189e39 100644 --- a/tests/markets/test_markets.py +++ b/tests/markets/test_markets.py @@ -4,6 +4,7 @@ from prediction_market_agent_tooling.markets.agent_market import ( AgentMarket, FilterBy, + MarketFees, SortBy, ) from prediction_market_agent_tooling.markets.markets import ( @@ -32,6 +33,7 @@ def test_valid_token_pool() -> None: current_p_yes=Probability(0.5), url="https://example.com", volume=None, + fees=MarketFees.get_zero_fees(), ) assert market.has_token_pool() is True assert market.get_pool_tokens("yes") == 1.1 @@ -52,6 +54,7 @@ def test_invalid_token_pool() -> None: current_p_yes=Probability(0.5), url="https://example.com", volume=None, + fees=MarketFees.get_zero_fees(), ) assert "do not match outcomes" in str(e.value) diff --git a/tests/tools/test_utils.py b/tests/tools/test_utils.py index 00b1666c..d635a2ea 100644 --- a/tests/tools/test_utils.py +++ b/tests/tools/test_utils.py @@ -1,6 +1,7 @@ import numpy as np import pytest +from prediction_market_agent_tooling.markets.market_fees import MarketFees from prediction_market_agent_tooling.tools.utils import ( calculate_sell_amount_in_collateral, ) @@ -13,7 +14,7 @@ def test_calculate_sell_amount_in_collateral_0() -> None: shares_to_sell=1, holdings=1000000000000 - 1, other_holdings=1000000000000, - fee=0, + fees=MarketFees.get_zero_fees(), ) assert np.isclose(collateral, 0.5) @@ -24,7 +25,7 @@ def test_calculate_sell_amount_in_collateral_1() -> None: shares_to_sell=1, holdings=10000000000000, other_holdings=1, - fee=0, + fees=MarketFees.get_zero_fees(), ) assert np.isclose(near_zero_collateral, 0) @@ -33,43 +34,45 @@ def test_calculate_sell_amount_in_collateral_1() -> None: shares_to_sell=1, holdings=1, other_holdings=10000000000000, - fee=0, + fees=MarketFees.get_zero_fees(), ) 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: + def get_collateral(bet_proportion_fee: float) -> float: + fees = MarketFees.get_zero_fees(bet_proportion=bet_proportion_fee) return calculate_sell_amount_in_collateral( shares_to_sell=2.5, holdings=10, other_holdings=3, - fee=fee, + fees=fees, ) - c1 = get_collateral(fee=0.1) - c2 = get_collateral(fee=0.35) + c1 = get_collateral(bet_proportion_fee=0.1) + c2 = get_collateral(bet_proportion_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: + def get_collateral(bet_proportion_fee: float) -> float: + fees = MarketFees.get_zero_fees(bet_proportion=bet_proportion_fee) return calculate_sell_amount_in_collateral( shares_to_sell=2.5, holdings=10, other_holdings=3, - fee=fee, + fees=fees, ) with pytest.raises(ValueError) as e: - get_collateral(fee=-0.1) - assert str(e.value) == "Fee must be between 0 and 1" + get_collateral(bet_proportion_fee=-0.1) + assert "Input should be greater than or equal to 0" in str(e.value) with pytest.raises(ValueError) as e: - get_collateral(fee=1.0) - assert str(e.value) == "Fee must be between 0 and 1" + get_collateral(bet_proportion_fee=1.0) + assert "Input should be less than 1" in str(e.value) def test_calculate_sell_amount_in_collateral_4() -> None: @@ -78,6 +81,6 @@ def test_calculate_sell_amount_in_collateral_4() -> None: shares_to_sell=100, holdings=10, other_holdings=0, - fee=0, + fees=MarketFees.get_zero_fees(), ) assert str(e.value) == "All share args must be greater than 0" diff --git a/tests_integration/markets/omen/test_kelly.py b/tests_integration/markets/omen/test_kelly.py index 1871d70f..5b3f7717 100644 --- a/tests_integration/markets/omen/test_kelly.py +++ b/tests_integration/markets/omen/test_kelly.py @@ -2,7 +2,11 @@ import pytest from prediction_market_agent_tooling.deploy.betting_strategy import KellyBettingStrategy -from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy +from prediction_market_agent_tooling.markets.agent_market import ( + FilterBy, + MarketFees, + SortBy, +) from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import ( OmenSubgraphHandler, @@ -57,7 +61,7 @@ def test_kelly_price_impact_works_small_pool( max_bet_amount: int, max_price_impact: float, p_yes: float ) -> None: large_market = OmenSubgraphHandler().get_omen_binary_markets_simple( - limit=2, filter_by=FilterBy.OPEN, sort_by=SortBy.LOWEST_LIQUIDITY + limit=1, filter_by=FilterBy.OPEN, sort_by=SortBy.LOWEST_LIQUIDITY )[0] omen_agent_market = OmenAgentMarket.from_data_model(large_market) confidence = 1.0 @@ -87,6 +91,7 @@ def assert_price_impact_converges( estimated_p_yes=p_yes, max_bet=max_bet_amount, confidence=confidence, + fees=omen_agent_market.fees, ) kelly = KellyBettingStrategy( @@ -94,14 +99,14 @@ def assert_price_impact_converges( ) max_price_impact_bet_amount = kelly.calculate_bet_amount_for_price_impact( - omen_agent_market, kelly_bet, 0 + omen_agent_market, kelly_bet ) price_impact = kelly.calculate_price_impact_for_bet_amount( kelly_bet.direction, bet_amount=max_price_impact_bet_amount, yes=yes_outcome_pool_size, no=no_outcome_pool_size, - fee=0, + fees=omen_agent_market.fees, ) # assert convergence @@ -116,7 +121,11 @@ def assert_price_impact( kelly: KellyBettingStrategy, ) -> None: price_impact = kelly.calculate_price_impact_for_bet_amount( - buy_direction, bet_amount=bet_amount, yes=yes, no=no, fee=0 + buy_direction, + bet_amount=bet_amount, + yes=yes, + no=no, + fees=MarketFees.get_zero_fees(), ) # Calculation is done assuming buy_direction is True. Else, we invert the reserves. diff --git a/tests_integration/tools/ipfs/test_ipfs_handler.py b/tests_integration/tools/ipfs/test_ipfs_handler.py index 93906f2b..77444353 100644 --- a/tests_integration/tools/ipfs/test_ipfs_handler.py +++ b/tests_integration/tools/ipfs/test_ipfs_handler.py @@ -1,4 +1,3 @@ -import datetime import typing as t from tempfile import NamedTemporaryFile @@ -7,6 +6,8 @@ from prediction_market_agent_tooling.config import APIKeys from prediction_market_agent_tooling.tools.ipfs.ipfs_handler import IPFSHandler +from prediction_market_agent_tooling.tools.utils import utcnow +from tests.utils import RUN_PAID_TESTS @pytest.fixture(scope="module") @@ -15,9 +16,10 @@ def test_ipfs_handler() -> t.Generator[IPFSHandler, None, None]: yield IPFSHandler(keys) +@pytest.mark.skipif(not RUN_PAID_TESTS, reason="This test costs money to run.") def test_ipfs_upload_and_removal(test_ipfs_handler: IPFSHandler) -> None: # We add the current datetime to avoid uploading an existing file (CID is content-based) - temp_string = f"Hello World {datetime.datetime.utcnow()}" + temp_string = f"Hello World {utcnow()}" with NamedTemporaryFile() as temp_file: temp_file.write(temp_string.encode("utf-8")) temp_file.flush()