From e91c6c127d6f14e1a0ac2cee85cfc2a62c7dd2a0 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 10:35:46 +0200 Subject: [PATCH 1/8] Split `DeployableTraderAgent` into `DeployablePredictionAgent` --- .../deploy/agent.py | 198 +++++++++++------- .../markets/agent_market.py | 13 +- .../markets/metaculus/metaculus.py | 19 +- .../markets/omen/omen.py | 20 +- 4 files changed, 162 insertions(+), 88 deletions(-) diff --git a/prediction_market_agent_tooling/deploy/agent.py b/prediction_market_agent_tooling/deploy/agent.py index 0a282aa1..3f5f4eae 100644 --- a/prediction_market_agent_tooling/deploy/agent.py +++ b/prediction_market_agent_tooling/deploy/agent.py @@ -36,6 +36,7 @@ AgentMarket, FilterBy, ProcessedMarket, + ProcessedTradedMarket, SortBy, ) from prediction_market_agent_tooling.markets.data_models import ( @@ -274,11 +275,13 @@ def get_gcloud_fname(self, market_type: MarketType) -> str: return f"{self.__class__.__name__.lower()}-{market_type}-{utcnow().strftime('%Y-%m-%d--%H-%M-%S')}" -class DeployableTraderAgent(DeployableAgent): +class DeployablePredictionAgent(DeployableAgent): bet_on_n_markets_per_run: int = 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) + # Only Metaculus allows to post predictions without trading (buying/selling of outcome tokens). + supported_markets: t.Sequence[MarketType] = [MarketType.METACULUS] def __init__( self, @@ -288,15 +291,6 @@ def __init__( super().__init__(enable_langfuse=enable_langfuse) self.place_bet = place_bet - def get_betting_strategy(self, market: AgentMarket) -> BettingStrategy: - user_id = market.get_user_id(api_keys=APIKeys()) - - total_amount = market.get_tiny_bet_amount().amount - if existing_position := market.get_position(user_id=user_id): - total_amount += existing_position.total_amount.amount - - return MaxAccuracyBettingStrategy(bet_amount=total_amount) - def initialize_langfuse(self) -> None: super().initialize_langfuse() # Auto-observe all the methods where it makes sense, so that subclassses don't need to do it manually. @@ -304,7 +298,6 @@ def initialize_langfuse(self) -> None: self.verify_market = observe()(self.verify_market) # type: ignore[method-assign] self.answer_binary_market = observe()(self.answer_binary_market) # type: ignore[method-assign] self.process_market = observe()(self.process_market) # type: ignore[method-assign] - self.build_trades = observe()(self.build_trades) # type: ignore[method-assign] def update_langfuse_trace_by_market( self, market_type: MarketType, market: AgentMarket @@ -342,19 +335,6 @@ def check_min_required_balance_to_operate(self, market_type: MarketType) -> None f"{api_keys=} doesn't have enough operational balance." ) - 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 {min_required_balance_to_trade} for agent is not met." - ) - def have_bet_on_market_since(self, market: AgentMarket, since: timedelta) -> bool: return have_bet_on_market_since(keys=APIKeys(), market=market, since=since) @@ -399,17 +379,6 @@ def get_markets( ) return available_markets - def build_trades( - self, - market: AgentMarket, - answer: ProbabilisticAnswer, - existing_position: Position | None, - ) -> list[Trade]: - strategy = self.get_betting_strategy(market=market) - trades = strategy.calculate_trades(existing_position, answer, market) - BettingStrategy.assert_trades_currency_match_markets(market, trades) - return trades - def before_process_market( self, market_type: MarketType, market: AgentMarket ) -> None: @@ -417,8 +386,6 @@ def before_process_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: @@ -436,62 +403,28 @@ def process_market( ) -> ProcessedMarket | None: logger.info(f"Processing market {market.question=} from {market.url=}.") - self.before_process_market(market_type, market) - if verify_market and not self.verify_market(market_type, market): logger.info(f"Market '{market.question}' doesn't meet the criteria.") - self.update_langfuse_trace_by_processed_market(market_type, None) return None answer = self.answer_binary_market(market) - - if answer is None: - logger.info(f"No answer for market '{market.question}'.") - self.update_langfuse_trace_by_processed_market(market_type, None) - return None - - existing_position = market.get_position(user_id=APIKeys().bet_from_address) - trades = self.build_trades( - market=market, - answer=answer, - existing_position=existing_position, + processed_market = ( + ProcessedMarket(answer=answer) if answer is not None else None ) - placed_trades = [] - if self.place_bet: - for trade in trades: - logger.info(f"Executing trade {trade} on market {market.id}") - - match trade.trade_type: - case TradeType.BUY: - id = market.buy_tokens( - outcome=trade.outcome, amount=trade.amount - ) - case TradeType.SELL: - id = market.sell_tokens( - outcome=trade.outcome, amount=trade.amount - ) - case _: - raise ValueError(f"Unexpected trade type {trade.trade_type}.") - placed_trades.append(PlacedTrade.from_trade(trade, id)) - - processed_market = ProcessedMarket(answer=answer, trades=placed_trades) - self.update_langfuse_trace_by_processed_market(market_type, processed_market) - - self.after_process_market( - market_type, market, processed_market=processed_market + logger.info( + f"Processed market {market.question=} from {market.url=} with {answer=}." ) - - logger.info(f"Processed market {market.question=} from {market.url=}.") return processed_market def after_process_market( self, market_type: MarketType, market: AgentMarket, - processed_market: ProcessedMarket, + processed_market: ProcessedMarket | None, ) -> None: keys = APIKeys() + self.update_langfuse_trace_by_processed_market(market_type, processed_market) market.store_prediction(processed_market=processed_market, keys=keys) def before_process_markets(self, market_type: MarketType) -> None: @@ -514,7 +447,9 @@ def process_markets(self, market_type: MarketType) -> None: processed = 0 for market in available_markets: + self.before_process_market(market_type, market) processed_market = self.process_market(market_type, market) + self.after_process_market(market_type, market, processed_market) if processed_market is not None: processed += 1 @@ -528,6 +463,115 @@ def after_process_markets(self, market_type: MarketType) -> None: "Executes actions that occur after bets are placed." def run(self, market_type: MarketType) -> None: + if market_type not in self.supported_markets: + raise ValueError( + f"Only {self.supported_markets} are supported by this agent." + ) self.before_process_markets(market_type) self.process_markets(market_type) self.after_process_markets(market_type) + + +class DeployableTraderAgent(DeployablePredictionAgent): + # These markets require place of bet, not just predictions. + supported_markets = [MarketType.OMEN, MarketType.MANIFOLD, MarketType.POLYMARKET] + + def initialize_langfuse(self) -> None: + super().initialize_langfuse() + # Auto-observe all the methods where it makes sense, so that subclassses don't need to do it manually. + self.get_betting_strategy = observe()(self.get_betting_strategy) # type: ignore[method-assign] + self.build_trades = observe()(self.build_trades) # type: ignore[method-assign] + + 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 {min_required_balance_to_trade} for agent is not met." + ) + + def get_betting_strategy(self, market: AgentMarket) -> BettingStrategy: + user_id = market.get_user_id(api_keys=APIKeys()) + + total_amount = market.get_tiny_bet_amount().amount + if existing_position := market.get_position(user_id=user_id): + total_amount += existing_position.total_amount.amount + + return MaxAccuracyBettingStrategy(bet_amount=total_amount) + + def build_trades( + self, + market: AgentMarket, + answer: ProbabilisticAnswer, + existing_position: Position | None, + ) -> list[Trade]: + strategy = self.get_betting_strategy(market=market) + trades = strategy.calculate_trades(existing_position, answer, market) + BettingStrategy.assert_trades_currency_match_markets(market, trades) + return trades + + def before_process_market( + self, market_type: MarketType, market: AgentMarket + ) -> None: + super().before_process_market(market_type, market) + self.check_min_required_balance_to_trade(market) + + def process_market( + self, + market_type: MarketType, + market: AgentMarket, + verify_market: bool = True, + ) -> ProcessedTradedMarket | None: + processed_market = super().process_market(market_type, market, verify_market) + if processed_market is None: + return None + + api_keys = APIKeys() + existing_position = market.get_position( + user_id=market.get_user_id(api_keys=api_keys) + ) + trades = self.build_trades( + market=market, + answer=processed_market.answer, + existing_position=existing_position, + ) + + placed_trades = [] + if self.place_bet: + for trade in trades: + logger.info(f"Executing trade {trade} on market {market.id}") + + match trade.trade_type: + case TradeType.BUY: + id = market.buy_tokens( + outcome=trade.outcome, amount=trade.amount + ) + case TradeType.SELL: + id = market.sell_tokens( + outcome=trade.outcome, amount=trade.amount + ) + case _: + raise ValueError(f"Unexpected trade type {trade.trade_type}.") + placed_trades.append(PlacedTrade.from_trade(trade, id)) + + traded_market = ProcessedTradedMarket( + answer=processed_market.answer, trades=placed_trades + ) + logger.info(f"Traded market {market.question=} from {market.url=}.") + return traded_market + + def after_process_market( + self, + market_type: MarketType, + market: AgentMarket, + processed_market: ProcessedMarket | ProcessedTradedMarket | None, + ) -> None: + api_keys = APIKeys() + super().after_process_market(market_type, market, processed_market) + if isinstance(processed_market, ProcessedTradedMarket): + market.store_trades(processed_market, api_keys) diff --git a/prediction_market_agent_tooling/markets/agent_market.py b/prediction_market_agent_tooling/markets/agent_market.py index 713057de..8344e5e9 100644 --- a/prediction_market_agent_tooling/markets/agent_market.py +++ b/prediction_market_agent_tooling/markets/agent_market.py @@ -29,6 +29,9 @@ class ProcessedMarket(BaseModel): answer: ProbabilisticAnswer + + +class ProcessedTradedMarket(ProcessedMarket): trades: list[PlacedTrade] @@ -228,13 +231,21 @@ def verify_operational_balance(api_keys: APIKeys) -> bool: raise NotImplementedError("Subclasses must implement this method") def store_prediction( - self, processed_market: ProcessedMarket, keys: APIKeys + self, processed_market: ProcessedMarket | None, keys: APIKeys ) -> None: """ If market allows to upload predictions somewhere, implement it in this method. """ raise NotImplementedError("Subclasses must implement this method") + def store_trades( + self, traded_market: ProcessedTradedMarket | None, keys: APIKeys + ) -> None: + """ + If market allows to upload trades 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 diff --git a/prediction_market_agent_tooling/markets/metaculus/metaculus.py b/prediction_market_agent_tooling/markets/metaculus/metaculus.py index fe52e4ef..e4579aec 100644 --- a/prediction_market_agent_tooling/markets/metaculus/metaculus.py +++ b/prediction_market_agent_tooling/markets/metaculus/metaculus.py @@ -1,11 +1,11 @@ import typing as t from prediction_market_agent_tooling.config import APIKeys -from prediction_market_agent_tooling.gtypes import Probability from prediction_market_agent_tooling.markets.agent_market import ( AgentMarket, FilterBy, MarketFees, + ProcessedMarket, SortBy, ) from prediction_market_agent_tooling.markets.metaculus.api import ( @@ -17,7 +17,7 @@ from prediction_market_agent_tooling.markets.metaculus.data_models import ( MetaculusQuestion, ) -from prediction_market_agent_tooling.tools.utils import DatetimeUTC +from prediction_market_agent_tooling.tools.utils import DatetimeUTC, check_not_none class MetaculusAgentMarket(AgentMarket): @@ -103,9 +103,18 @@ def get_binary_markets( break return [MetaculusAgentMarket.from_data_model(q) for q in all_questions[:limit]] - def submit_prediction(self, p_yes: Probability, reasoning: str) -> None: - make_prediction(self.id, p_yes) - post_question_comment(self.id, reasoning) + def store_prediction( + self, processed_market: ProcessedMarket | None, keys: APIKeys + ) -> None: + if processed_market is not None: + make_prediction(self.id, processed_market.answer.p_yes) + post_question_comment( + self.id, + check_not_none( + processed_market.answer.reasoning, + "Reasoning must be provided for Metaculus.", + ), + ) @staticmethod def get_user_id(api_keys: APIKeys) -> str: diff --git a/prediction_market_agent_tooling/markets/omen/omen.py b/prediction_market_agent_tooling/markets/omen/omen.py index c4685c2c..33373d5c 100644 --- a/prediction_market_agent_tooling/markets/omen/omen.py +++ b/prediction_market_agent_tooling/markets/omen/omen.py @@ -25,6 +25,7 @@ FilterBy, MarketFees, ProcessedMarket, + ProcessedTradedMarket, SortBy, ) from prediction_market_agent_tooling.markets.data_models import ( @@ -417,11 +418,20 @@ def verify_operational_balance(api_keys: APIKeys) -> bool: ) > xdai_type(0.001) def store_prediction( - self, processed_market: ProcessedMarket, keys: APIKeys + self, processed_market: ProcessedMarket | None, keys: APIKeys ) -> None: + """On Omen, we have to store predictions along with trades, see `store_trades`.""" + + def store_trades( + self, traded_market: ProcessedTradedMarket | None, keys: APIKeys + ) -> None: + if traded_market is None: + logger.warning(f"No prediction for market {self.id}, not storing anything.") + return + reasoning = ( - processed_market.answer.reasoning - if processed_market.answer.reasoning + traded_market.answer.reasoning + if traded_market.answer and traded_market.answer.reasoning else "" ) @@ -434,13 +444,13 @@ def store_prediction( 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 + HexBytes(HexStr(i.id)) for i in traded_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), + estimated_probability_bps=int(traded_market.answer.p_yes * 10000), ) tx_receipt = OmenAgentResultMappingContract().add_prediction( api_keys=keys, From 555506b9829110777aef2fb6e173fc66032d6844 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 10:39:58 +0200 Subject: [PATCH 2/8] add test --- tests/markets/test_markets.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/markets/test_markets.py b/tests/markets/test_markets.py index 89189e39..7f338260 100644 --- a/tests/markets/test_markets.py +++ b/tests/markets/test_markets.py @@ -1,5 +1,9 @@ import pytest +from prediction_market_agent_tooling.deploy.agent import ( + DeployablePredictionAgent, + DeployableTraderAgent, +) from prediction_market_agent_tooling.gtypes import Probability from prediction_market_agent_tooling.markets.agent_market import ( AgentMarket, @@ -86,3 +90,11 @@ def test_get_markets(market_type: MarketType) -> None: limit=limit, sort_by=SortBy.NONE, filter_by=FilterBy.OPEN ) assert len(markets) <= limit + + +@pytest.mark.parametrize("market_type", list(MarketType)) +def test_market_is_covered(market_type: MarketType) -> None: + assert ( + market_type in DeployablePredictionAgent.supported_markets + or market_type in DeployableTraderAgent.supported_markets + ), f"Market {market_type} isn't supported in any of our deployable agents." From 7080a56a5bee2f465a154a815abd9694ac42756e Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 10:57:03 +0200 Subject: [PATCH 3/8] fix __init_subclass__ --- prediction_market_agent_tooling/deploy/agent.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/prediction_market_agent_tooling/deploy/agent.py b/prediction_market_agent_tooling/deploy/agent.py index a9701d88..ef29b7ca 100644 --- a/prediction_market_agent_tooling/deploy/agent.py +++ b/prediction_market_agent_tooling/deploy/agent.py @@ -166,9 +166,11 @@ def session_id(self) -> str: return f"{self.__class__.__name__} - {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}" def __init_subclass__(cls, **kwargs: t.Any) -> None: - if "DeployableAgent" not in str( - cls.__init__ - ) and "DeployableTraderAgent" not in str(cls.__init__): + if ( + "DeployableAgent" not in str(cls.__init__) + and "DeployableTraderAgent" not in str(cls.__init__) + and "DeployablePredictionAgent" not in str(cls.__init__) + ): raise TypeError( "Cannot override __init__ method of deployable agent class, please override the `load` method to set up the agent." ) From 12cc53967fb4091a8ed32fb1adf2abb297b385a4 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 11:26:34 +0200 Subject: [PATCH 4/8] Bump ver --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4dee2f43..db6d2aae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "prediction-market-agent-tooling" -version = "0.54.0" +version = "0.55.0" description = "Tools to benchmark, deploy and monitor prediction market agents." authors = ["Gnosis"] readme = "README.md" From 170412ff7b563094a6d14ce72a2046c4a76480db Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 12:17:20 +0200 Subject: [PATCH 5/8] fix type --- prediction_market_agent_tooling/deploy/agent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/prediction_market_agent_tooling/deploy/agent.py b/prediction_market_agent_tooling/deploy/agent.py index ef29b7ca..67cd2605 100644 --- a/prediction_market_agent_tooling/deploy/agent.py +++ b/prediction_market_agent_tooling/deploy/agent.py @@ -476,7 +476,11 @@ def run(self, market_type: MarketType) -> None: class DeployableTraderAgent(DeployablePredictionAgent): # These markets require place of bet, not just predictions. - supported_markets = [MarketType.OMEN, MarketType.MANIFOLD, MarketType.POLYMARKET] + supported_markets: t.Sequence[MarketType] = [ + MarketType.OMEN, + MarketType.MANIFOLD, + MarketType.POLYMARKET, + ] def initialize_langfuse(self) -> None: super().initialize_langfuse() From f36a9c3193988b72f774d23dc8f26fb5b2392815 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 13:04:54 +0200 Subject: [PATCH 6/8] remove explicit typing --- prediction_market_agent_tooling/deploy/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prediction_market_agent_tooling/deploy/agent.py b/prediction_market_agent_tooling/deploy/agent.py index 67cd2605..0510bb29 100644 --- a/prediction_market_agent_tooling/deploy/agent.py +++ b/prediction_market_agent_tooling/deploy/agent.py @@ -577,7 +577,7 @@ def after_process_market( self, market_type: MarketType, market: AgentMarket, - processed_market: ProcessedMarket | ProcessedTradedMarket | None, + processed_market: ProcessedMarket | None, ) -> None: api_keys = APIKeys() super().after_process_market(market_type, market, processed_market) From 47381873c5cae7e86de08914befe7338c909d5d1 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 13:16:45 +0200 Subject: [PATCH 7/8] Fix betting benchmark --- .../match_bets_with_langfuse_traces.py | 20 +++++++++++-------- .../deploy/betting_strategy.py | 6 +++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/examples/monitor/match_bets_with_langfuse_traces.py b/examples/monitor/match_bets_with_langfuse_traces.py index 11f09244..b048777f 100644 --- a/examples/monitor/match_bets_with_langfuse_traces.py +++ b/examples/monitor/match_bets_with_langfuse_traces.py @@ -8,6 +8,7 @@ from prediction_market_agent_tooling.config import APIKeys from prediction_market_agent_tooling.deploy.betting_strategy import ( BettingStrategy, + GuaranteedLossError, KellyBettingStrategy, MaxAccuracyBettingStrategy, MaxAccuracyWithKellyScaledBetsStrategy, @@ -50,14 +51,17 @@ def get_outcome_for_trace( market = trace.market answer = trace.answer - trades = strategy.calculate_trades( - existing_position=None, - answer=ProbabilisticAnswer( - p_yes=answer.p_yes, - confidence=answer.confidence, - ), - market=market, - ) + try: + trades = strategy.calculate_trades( + existing_position=None, + answer=ProbabilisticAnswer( + p_yes=answer.p_yes, + confidence=answer.confidence, + ), + market=market, + ) + except GuaranteedLossError: + return None # For example, when our predicted p_yes is 95%, but market is already trading at 99%, and we don't have anything to sell, Kelly will yield no trades. if not trades: return None diff --git a/prediction_market_agent_tooling/deploy/betting_strategy.py b/prediction_market_agent_tooling/deploy/betting_strategy.py index f4761b37..f1ec7f1e 100644 --- a/prediction_market_agent_tooling/deploy/betting_strategy.py +++ b/prediction_market_agent_tooling/deploy/betting_strategy.py @@ -24,6 +24,10 @@ from prediction_market_agent_tooling.tools.utils import check_not_none +class GuaranteedLossError(RuntimeError): + pass + + class BettingStrategy(ABC): @abstractmethod def calculate_trades( @@ -63,7 +67,7 @@ def assert_buy_trade_wont_be_guaranteed_loss( ) if outcome_tokens_to_get.amount < trade.amount.amount: - raise RuntimeError( + raise GuaranteedLossError( f"Trade {trade=} would result in guaranteed loss by getting only {outcome_tokens_to_get=}." ) From f455f3e724c45a09f1ee340be741d47405205032 Mon Sep 17 00:00:00 2001 From: Peter Jung Date: Wed, 23 Oct 2024 14:21:39 +0200 Subject: [PATCH 8/8] Fix tracing and place_bet argument --- .../deploy/agent.py | 41 +++++++++++++++---- .../markets/metaculus/metaculus.py | 10 +++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/prediction_market_agent_tooling/deploy/agent.py b/prediction_market_agent_tooling/deploy/agent.py index 0510bb29..6e451f2e 100644 --- a/prediction_market_agent_tooling/deploy/agent.py +++ b/prediction_market_agent_tooling/deploy/agent.py @@ -288,10 +288,10 @@ class DeployablePredictionAgent(DeployableAgent): def __init__( self, enable_langfuse: bool = APIKeys().default_enable_langfuse, - place_bet: bool = True, + store_prediction: bool = True, ) -> None: super().__init__(enable_langfuse=enable_langfuse) - self.place_bet = place_bet + self.store_prediction = store_prediction def initialize_langfuse(self) -> None: super().initialize_langfuse() @@ -384,8 +384,6 @@ def get_markets( def before_process_market( self, market_type: MarketType, market: AgentMarket ) -> None: - self.update_langfuse_trace_by_market(market_type, market) - api_keys = APIKeys() if market_type.is_blockchain_market: @@ -403,6 +401,7 @@ def process_market( market: AgentMarket, verify_market: bool = True, ) -> ProcessedMarket | None: + self.update_langfuse_trace_by_market(market_type, market) logger.info(f"Processing market {market.question=} from {market.url=}.") if verify_market and not self.verify_market(market_type, market): @@ -414,6 +413,7 @@ def process_market( ProcessedMarket(answer=answer) if answer is not None else None ) + self.update_langfuse_trace_by_processed_market(market_type, processed_market) logger.info( f"Processed market {market.question=} from {market.url=} with {answer=}." ) @@ -426,8 +426,12 @@ def after_process_market( processed_market: ProcessedMarket | None, ) -> None: keys = APIKeys() - self.update_langfuse_trace_by_processed_market(market_type, processed_market) - market.store_prediction(processed_market=processed_market, keys=keys) + if self.store_prediction: + market.store_prediction(processed_market=processed_market, keys=keys) + else: + logger.info( + f"Prediction {processed_market} not stored because {self.store_prediction=}." + ) def before_process_markets(self, market_type: MarketType) -> None: """ @@ -482,6 +486,20 @@ class DeployableTraderAgent(DeployablePredictionAgent): MarketType.POLYMARKET, ] + def __init__( + self, + enable_langfuse: bool = APIKeys().default_enable_langfuse, + store_prediction: bool = True, + store_trades: bool = True, + place_trades: bool = True, + ) -> None: + super().__init__( + enable_langfuse=enable_langfuse, store_prediction=store_prediction + ) + self.store_prediction = store_prediction + self.store_trades = store_trades + self.place_trades = place_trades + def initialize_langfuse(self) -> None: super().initialize_langfuse() # Auto-observe all the methods where it makes sense, so that subclassses don't need to do it manually. @@ -551,7 +569,7 @@ def process_market( for trade in trades: logger.info(f"Executing trade {trade} on market {market.id} ({market.url})") - if self.place_bet: + if self.place_trades: match trade.trade_type: case TradeType.BUY: id = market.buy_tokens( @@ -565,7 +583,7 @@ def process_market( raise ValueError(f"Unexpected trade type {trade.trade_type}.") placed_trades.append(PlacedTrade.from_trade(trade, id)) else: - logger.info(f"Trade execution skipped because {self.place_bet=}.") + logger.info(f"Trade execution skipped because {self.place_trades=}.") traded_market = ProcessedTradedMarket( answer=processed_market.answer, trades=placed_trades @@ -582,4 +600,9 @@ def after_process_market( api_keys = APIKeys() super().after_process_market(market_type, market, processed_market) if isinstance(processed_market, ProcessedTradedMarket): - market.store_trades(processed_market, api_keys) + if self.store_trades: + market.store_trades(processed_market, api_keys) + else: + logger.info( + f"Trades {processed_market.trades} not stored because {self.store_trades=}." + ) diff --git a/prediction_market_agent_tooling/markets/metaculus/metaculus.py b/prediction_market_agent_tooling/markets/metaculus/metaculus.py index e4579aec..8dd3bfe3 100644 --- a/prediction_market_agent_tooling/markets/metaculus/metaculus.py +++ b/prediction_market_agent_tooling/markets/metaculus/metaculus.py @@ -119,3 +119,13 @@ def store_prediction( @staticmethod def get_user_id(api_keys: APIKeys) -> str: return str(api_keys.metaculus_user_id) + + @staticmethod + def verify_operational_balance(api_keys: APIKeys) -> bool: + # No operational balance for Metaculus. + return True + + @staticmethod + def redeem_winnings(api_keys: APIKeys) -> None: + # Nothing to redeem on Metaculus. + pass