Skip to content

Commit

Permalink
Introduce fees into AgentMarket itself (not just OmenAgentMarket) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
kongzii authored Oct 17, 2024
1 parent 8fd2577 commit 5ed2712
Show file tree
Hide file tree
Showing 21 changed files with 176 additions and 63 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,5 @@ cython_debug/

tests_files/*
!tests_files/.gitkeep

bet_strategy_benchmark*
15 changes: 12 additions & 3 deletions examples/monitor/match_bets_with_langfuse_traces.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any

import pandas as pd
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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)
16 changes: 10 additions & 6 deletions prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -178,7 +178,7 @@ def calculate_trades(
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.
Expand All @@ -196,15 +196,20 @@ 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 = (
no / total_outcome_tokens if buy_direction else yes / total_outcome_tokens
)

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
Expand All @@ -216,7 +221,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,
Expand All @@ -226,7 +230,7 @@ def calculate_price_impact_deviation_from_target_price_impact(
bet_amount,
yes_outcome_pool_size,
no_outcome_pool_size,
fee,
MarketFees.get_zero_fees(), # TODO: Use market.fees
)
# We return abs for the algorithm to converge to 0 instead of the min (and possibly negative) value.

Expand Down
2 changes: 1 addition & 1 deletion prediction_market_agent_tooling/jobs/omen/omen_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)


Expand Down
12 changes: 11 additions & 1 deletion prediction_market_agent_tooling/markets/agent_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions prediction_market_agent_tooling/markets/manifold/manifold.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions prediction_market_agent_tooling/markets/market_fees.py
Original file line number Diff line number Diff line change
@@ -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 - self.total_fee_absolute_value(bet_amount)
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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":
Expand Down
23 changes: 13 additions & 10 deletions prediction_market_agent_tooling/markets/omen/omen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand All @@ -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,
Expand Down Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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 = MarketFees.get_zero_fees(), # TODO: Remove default value.
) -> SimpleBet:
"""
Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 5ed2712

Please sign in to comment.