Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate AgentMarket.outcome_token_pool into betting strategies #389

Merged
merged 17 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
)
from prediction_market_agent_tooling.markets.omen.data_models import get_boolean_outcome
from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
get_kelly_bet,
get_kelly_bet_full,
get_kelly_bet_simplified,
)
from prediction_market_agent_tooling.tools.utils import check_not_none


class BettingStrategy(ABC):
Expand Down Expand Up @@ -168,11 +170,26 @@ def calculate_trades(
market: AgentMarket,
) -> list[Trade]:
adjusted_bet_amount = self.adjust_bet_amount(existing_position, market)
kelly_bet = get_kelly_bet(
adjusted_bet_amount,
market.current_p_yes,
answer.p_yes,
answer.confidence,
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=adjusted_bet_amount,
confidence=answer.confidence,
)
if market.has_token_pool()
else get_kelly_bet_simplified(
adjusted_bet_amount,
market.current_p_yes,
answer.p_yes,
answer.confidence,
)
)

amounts = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
from pydantic import BaseModel
from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet


def check_is_valid_probability(probability: float) -> None:
if not 0 <= probability <= 1:
raise ValueError("Probability must be between 0 and 1")


class KellyBet(BaseModel):
direction: bool
size: float


def get_kelly_bet(
def get_kelly_bet_simplified(
max_bet: float,
market_p_yes: float,
estimated_p_yes: float,
confidence: float,
) -> KellyBet:
) -> SimpleBet:
"""
Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.

Expand Down Expand Up @@ -57,4 +52,86 @@ def get_kelly_bet(
# Ensure bet size is non-negative does not exceed the wallet balance
bet_size = min(kelly_fraction * max_bet, max_bet)

return KellyBet(direction=bet_direction, size=bet_size)
return SimpleBet(direction=bet_direction, size=bet_size)


def get_kelly_bet_full(
yes_outcome_pool_size: float,
no_outcome_pool_size: float,
estimated_p_yes: float,
confidence: float,
max_bet: float,
fee: float = 0.0, # proportion, 0 to 1
) -> SimpleBet:
"""
Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.

'Full' as in it accounts for how the bet changes the market odds.

Taken from https://github.com/valory-xyz/trader/blob/main/strategies/kelly_criterion/kelly_criterion.py

with derivation in PR description: https://github.com/valory-xyz/trader/pull/119

```
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
"""
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)

x = yes_outcome_pool_size
y = no_outcome_pool_size
p = estimated_p_yes
c = confidence
b = max_bet
f = 1 - fee

numerator = (
-4 * x**2 * y
+ b * y**2 * p * c * f
+ 2 * b * x * y * p * c * f
+ b * x**2 * p * c * f
- 2 * b * y**2 * f
- 2 * b * x * y * f
+ (
(
4 * x**2 * y
- b * y**2 * p * c * f
- 2 * b * x * y * p * c * f
- b * x**2 * p * c * f
+ 2 * b * y**2 * f
+ 2 * b * x * y * f
)
** 2
- (
4
* (x**2 * f - y**2 * f)
* (
-4 * b * x * y**2 * p * c
- 4 * b * x**2 * y * p * c
+ 4 * b * x * y**2
)
)
)
** (1 / 2)
)
denominator = 2 * (x**2 * f - y**2 * f)
if denominator == 0:
return SimpleBet(direction=True, size=0)
kelly_bet_amount = numerator / denominator
Comment on lines +132 to +134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question about the formula: if (x=yes_outcome_pool_size = 10, y=no_outcome_pool_size=10), this always goes to 0 thus bet_amount is hard-set to 0.
Is that correct? This would e.g. prevent us from placing bets in balanced markets using Kelly - but maybe I'm mistaken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but that doesn't seem like desirable behaviour! Have made an issue to fix: #401


return SimpleBet(direction=kelly_bet_amount > 0, size=abs(kelly_bet_amount))
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
import typing as t
from functools import reduce

import numpy as np

from prediction_market_agent_tooling.gtypes import Probability, wei_type, xDai
from prediction_market_agent_tooling.loggers import logger
from prediction_market_agent_tooling.markets.omen.data_models import OmenMarket
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.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 (
ONE_XDAI,
wei_to_xdai,
xdai_to_wei,
)

OutcomeIndex = t.Literal[0, 1]
from prediction_market_agent_tooling.tools.web3_utils import wei_to_xdai, xdai_to_wei


def get_market_moving_bet(
market: OmenMarket,
target_p_yes: Probability,
yes_outcome_pool_size: float,
no_outcome_pool_size: float,
market_p_yes: float,
target_p_yes: float,
fee: float = 0.0, # proportion, 0 to 1
max_iters: int = 100,
check_vs_contract: bool = False, # Disable by default, as it's slow
verbose: bool = False,
) -> t.Tuple[xDai, OutcomeIndex]:
) -> SimpleBet:
"""
evangriffiths marked this conversation as resolved.
Show resolved Hide resolved
Implements a binary search to determine the bet that will move the market's
`p_yes` to that of the target.
Expand All @@ -43,74 +36,97 @@ def get_market_moving_bet(
new_product - fixed_product = dx * na_y
dx = (new_product - fixed_product) / na_y
"""
market_agent = OmenAgentMarket.from_data_model(market)
amounts = market.outcomeTokenAmounts
prices = check_not_none(
market.outcomeTokenProbabilities, "No probabilities, is marked closed?"
)
if len(amounts) != 2 or len(prices) != 2:
raise ValueError("Only binary markets are supported.")

fixed_product = reduce(lambda x, y: x * y, amounts, 1)
assert np.isclose(float(sum(prices)), 1)
fixed_product = yes_outcome_pool_size * no_outcome_pool_size
bet_direction: bool = target_p_yes > market_p_yes

# For FPMMs, the probability is equal to the marginal price
current_p_yes = Probability(prices[0])
bet_outcome_index: OutcomeIndex = 0 if target_p_yes > current_p_yes else 1

min_bet_amount = 0
max_bet_amount = 100 * sum(amounts) # TODO set a better upper bound
min_bet_amount = 0.0
max_bet_amount = 100 * (
yes_outcome_pool_size + no_outcome_pool_size
) # TODO set a better upper bound

# Binary search for the optimal bet amount
for _ in range(max_iters):
bet_amount = (min_bet_amount + max_bet_amount) // 2
bet_amount_ = (
bet_amount
* (
xdai_to_wei(ONE_XDAI)
- check_not_none(market.fee, "No fee for the market.")
)
/ xdai_to_wei(ONE_XDAI)
)
bet_amount = (min_bet_amount + max_bet_amount) / 2
amounts_diff = bet_amount * (1 - fee)

# Initial new amounts are old amounts + equal new amounts for each outcome
amounts_diff = bet_amount_
new_amounts = [amounts[i] + amounts_diff for i in range(len(amounts))]
yes_outcome_new_pool_size = yes_outcome_pool_size + amounts_diff
no_outcome_new_pool_size = no_outcome_pool_size + amounts_diff
new_amounts = {
True: yes_outcome_new_pool_size,
False: no_outcome_new_pool_size,
}

# Now give away tokens at `bet_outcome_index` to restore invariant
new_product = reduce(lambda x, y: x * y, new_amounts, 1.0)
dx = (new_product - fixed_product) / new_amounts[1 - bet_outcome_index]

# Sanity check the number of tokens against the contract
if check_vs_contract:
expected_trade = market_agent.get_contract().calcBuyAmount(
investment_amount=wei_type(bet_amount),
outcome_index=bet_outcome_index,
)
assert np.isclose(float(expected_trade), dx)

new_amounts[bet_outcome_index] -= dx
new_product = yes_outcome_new_pool_size * no_outcome_new_pool_size
dx = (new_product - fixed_product) / new_amounts[not bet_direction]
new_amounts[bet_direction] -= dx

# Check that the invariant is restored
assert np.isclose(
reduce(lambda x, y: x * y, new_amounts, 1.0), float(fixed_product)
reduce(lambda x, y: x * y, list(new_amounts.values()), 1.0),
float(fixed_product),
)
new_p_yes = Probability(new_amounts[1] / sum(new_amounts))
bet_amount_wei = wei_type(bet_amount)
if verbose:
outcome = market_agent.get_outcome_str(bet_outcome_index)
logger.debug(
f"Target p_yes: {target_p_yes:.2f}, bet: {wei_to_xdai(bet_amount_wei):.2f}{market_agent.currency} for {outcome}, new p_yes: {new_p_yes:.2f}"
)

new_p_yes = Probability(new_amounts[False] / sum(list(new_amounts.values())))
if abs(target_p_yes - new_p_yes) < 0.01:
break
elif new_p_yes > target_p_yes:
if bet_outcome_index == 0:
if bet_direction:
max_bet_amount = bet_amount
else:
min_bet_amount = bet_amount
else:
if bet_outcome_index == 0:
if bet_direction:
min_bet_amount = bet_amount
else:
max_bet_amount = bet_amount
return wei_to_xdai(bet_amount_wei), bet_outcome_index

return SimpleBet(direction=bet_direction, size=bet_amount)


def _sanity_check_omen_market_moving_bet(
bet_to_check: SimpleBet, market: OmenAgentMarket, target_p_yes: float
) -> None:
"""
A util function for checking that a bet moves the market to the target_p_yes
by calling the market's calcBuyAmount method from the smart contract, and
using the adjusted outcome pool sizes to calculate the new p_yes.
"""
buy_amount_ = market.get_contract().calcBuyAmount(
investment_amount=xdai_to_wei(xDai(bet_to_check.size)),
outcome_index=market.get_outcome_index(
market.get_outcome_str_from_bool(bet_to_check.direction)
),
)
buy_amount = float(wei_to_xdai(Wei(buy_amount_)))

outcome_token_pool = check_not_none(market.outcome_token_pool)
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)]
market_const = yes_outcome_pool_size * no_outcome_pool_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))
- float(bet_to_check.direction) * buy_amount
)
new_no_outcome_pool_size = (
no_outcome_pool_size
+ (bet_to_check.size * (1 - market.fee))
- float(not bet_to_check.direction) * buy_amount
)
new_market_const = new_yes_outcome_pool_size * new_no_outcome_pool_size
# Check the invariant is restored
assert np.isclose(new_market_const, market_const)

# Now check that the market's new p_yes is equal to the target_p_yes
new_p_yes = new_no_outcome_pool_size / (
new_yes_outcome_pool_size + new_no_outcome_pool_size
)
if not np.isclose(new_p_yes, target_p_yes, atol=0.01):
raise ValueError(
f"Bet does not move market to target_p_yes {target_p_yes=}. Got {new_p_yes=}"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class SimpleBet(BaseModel):
direction: bool
size: float
Loading
Loading