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

Add max_price_impact parameter to KellyBettingStrategy #433

Merged
merged 44 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4bfc702
WIP
gabrielfior Sep 24, 2024
2c8389a
Added Slippage-based Kelly strategy
gabrielfior Sep 25, 2024
c9c198a
Fixing PR
gabrielfior Sep 25, 2024
5333d1d
Removed file
gabrielfior Sep 25, 2024
195f019
Removed another file
gabrielfior Sep 25, 2024
fd741d4
Removed omen corrections
gabrielfior Sep 25, 2024
f7dc54b
WIP
gabrielfior Sep 26, 2024
9b6aaa7
Payouts increased
gabrielfior Sep 27, 2024
337df5e
Merge remote-tracking branch 'refs/remotes/origin/main' into gabriel/…
gabrielfior Sep 27, 2024
c17790d
Added tests for kelly slippage | PR comments
gabrielfior Sep 27, 2024
a6804a3
WIP
gabrielfior Sep 30, 2024
ac99ce1
Fetching bets with more error checking
gabrielfior Sep 30, 2024
bda388b
Fix examples/monitor/match_bets_with_langfuse_traces.py
evangriffiths Oct 1, 2024
752c3be
tidy
evangriffiths Oct 1, 2024
6c67f15
Review comment
evangriffiths Oct 1, 2024
f6a7499
Change match_bets_with_langfuse_traces.py to calculate p_yes MSE for …
evangriffiths Oct 1, 2024
0a01259
Removed dotenv
gabrielfior Oct 1, 2024
7a04ea6
Merge remote-tracking branch 'refs/remotes/origin/evan/fix-examples/m…
gabrielfior Oct 1, 2024
d13f235
Print correlation of p_yes mse with profit
evangriffiths Oct 1, 2024
55d90a6
Merge remote-tracking branch 'refs/remotes/origin/main' into gabriel/…
gabrielfior Oct 1, 2024
4b1a6d2
Sorting output file
gabrielfior Oct 1, 2024
297a0bf
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Oct 1, 2024
5ba66b5
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Oct 1, 2024
572b943
Merge remote-tracking branch 'refs/remotes/origin/evan/get_p_yes_mse'…
gabrielfior Oct 1, 2024
ba2afcd
Avoided price_impact warnings
gabrielfior Oct 1, 2024
dabcc23
Update prediction_market_agent_tooling/deploy/betting_strategy.py
gabrielfior Oct 1, 2024
1a3f362
Removed load_dotenv
gabrielfior Oct 1, 2024
625d012
Merge remote-tracking branch 'origin/gabriel/track-experiments-kelly'…
gabrielfior Oct 1, 2024
5e04adf
Tidy
gabrielfior Oct 1, 2024
3ccb430
Update .env.example
gabrielfior Oct 2, 2024
b7cd215
Implemented PR comments
gabrielfior Oct 2, 2024
321cff8
Switched back betting strategy to agreed upon impl
gabrielfior Oct 2, 2024
efa1909
Fixed mypy
gabrielfior Oct 2, 2024
06be0d6
Improved mypy removing ifs
gabrielfior Oct 2, 2024
79fca0c
Final mypy improvements
gabrielfior Oct 2, 2024
04ec90d
Merge remote-tracking branch 'refs/remotes/origin/main' into gabriel/…
gabrielfior Oct 2, 2024
b8884de
Moved test | refactored Yes,No
gabrielfior Oct 3, 2024
6b7b6a7
Importing logger from loggers and not loguru
gabrielfior Oct 3, 2024
83349d4
Made minimize_scalar bounds more robust
gabrielfior Oct 3, 2024
22e4028
Merge remote-tracking branch 'refs/remotes/origin/main' into gabriel/…
gabrielfior Oct 3, 2024
6f014a6
Small changes after merge
gabrielfior Oct 3, 2024
d3f5c04
Removed incorrect slippage mentions
gabrielfior Oct 3, 2024
74d524e
Refactored assert from betting stategy into integration test
gabrielfior Oct 3, 2024
8cd4818
Small fix test
gabrielfior Oct 3, 2024
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
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
MANIFOLD_API_KEY=
BET_FROM_PRIVATE_KEY=
OPENAI_API_KEY=
GRAPH_API_KEY=
GRAPH_API_KEY=
GOOGLE_APPLICATION_CREDENTIALS= #for loading secrets from GCP
37 changes: 35 additions & 2 deletions examples/monitor/match_bets_with_langfuse_traces.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
from datetime import datetime
from typing import Any

import hishel
from dotenv import load_dotenv

load_dotenv()
import pandas as pd
from langfuse import Langfuse
from pydantic import BaseModel

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.deploy.betting_strategy import (
BettingStrategy,
ProbabilisticAnswer,
TradeType,
KellyBettingStrategy,
MaxAccuracyBettingStrategy,
MaxAccuracyWithKellyScaledBetsStrategy,
MaxExpectedValueBettingStrategy,
ProbabilisticAnswer,
TradeType,
)
from prediction_market_agent_tooling.markets.data_models import ResolvedBet
from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
Expand Down Expand Up @@ -91,9 +95,11 @@ def get_outcome_for_trace(
# "DeployableThinkThoroughlyProphetResearchAgent": "pma-think-thoroughly-prophet-research", # no bets!
"DeployableKnownOutcomeAgent": "pma-knownoutcome",
}

agent_pkey_map = {
k: get_private_key_from_gcp_secret(v) for k, v in agent_gcp_secret_map.items()
}

# Define strategies we want to test out
strategies = [
MaxAccuracyBettingStrategy(bet_amount=1),
Expand All @@ -108,8 +114,34 @@ def get_outcome_for_trace(
MaxExpectedValueBettingStrategy(bet_amount=1),
MaxExpectedValueBettingStrategy(bet_amount=2),
MaxExpectedValueBettingStrategy(bet_amount=25),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.01),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.05),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.1),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.15),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.2),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.25),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.3),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.4),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.5),
KellyBettingStrategy(max_bet_amount=2, max_slippage=0.7),
KellyBettingStrategy(max_bet_amount=5, max_slippage=0.1),
KellyBettingStrategy(max_bet_amount=5, max_slippage=0.15),
KellyBettingStrategy(max_bet_amount=5, max_slippage=0.2),
KellyBettingStrategy(max_bet_amount=5, max_slippage=0.3),
KellyBettingStrategy(max_bet_amount=5, max_slippage=0.5),
KellyBettingStrategy(max_bet_amount=5, max_slippage=0.6),
KellyBettingStrategy(max_bet_amount=5, max_slippage=0.7),
KellyBettingStrategy(max_bet_amount=25, max_slippage=0.1),
KellyBettingStrategy(max_bet_amount=25, max_slippage=0.2),
KellyBettingStrategy(max_bet_amount=25, max_slippage=0.3),
KellyBettingStrategy(max_bet_amount=25, max_slippage=0.5),
KellyBettingStrategy(max_bet_amount=25, max_slippage=0.7),
]

storage = hishel.FileStorage(ttl=3600)
controller = hishel.Controller(force_cache=True)
httpx_client = hishel.CacheClient(storage=storage, controller=controller)
Copy link
Contributor

Choose a reason for hiding this comment

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

That's interesting, what about cutting it out to some helper function like get_cached_httpx_client?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Abstracted it away (see class HttpxCachedClient)


overall_md = ""

print("# Agent Bet vs Simulated Bet Comparison")
Expand All @@ -124,6 +156,7 @@ def get_outcome_for_trace(
secret_key=api_keys.langfuse_secret_key.get_secret_value(),
public_key=api_keys.langfuse_public_key,
host=api_keys.langfuse_host,
httpx_client=httpx_client,
)

traces = get_traces_for_agent(
Expand Down
95 changes: 91 additions & 4 deletions prediction_market_agent_tooling/deploy/betting_strategy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from abc import ABC, abstractmethod

import numpy as np
from loguru import logger
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
from loguru import logger
from prediction_market_agent_tooling.loggers import logger

Copy link
Contributor

Choose a reason for hiding this comment

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

You didn't commit this

from scipy.optimize import minimize_scalar

from prediction_market_agent_tooling.gtypes import xDai
from prediction_market_agent_tooling.markets.agent_market import AgentMarket
from prediction_market_agent_tooling.markets.data_models import (
Currency,
Expand All @@ -10,10 +15,14 @@
TradeType,
)
from prediction_market_agent_tooling.markets.omen.data_models import get_boolean_outcome
from prediction_market_agent_tooling.markets.omen.omen import (
get_buy_outcome_token_amount,
)
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


Expand Down Expand Up @@ -134,8 +143,27 @@ def calculate_direction(market_p_yes: float, estimate_p_yes: float) -> bool:


class KellyBettingStrategy(BettingStrategy):
def __init__(self, max_bet_amount: float):
def __init__(self, max_bet_amount: float, max_slippage: float | None = None):
self.max_bet_amount = max_bet_amount
self.max_price_impact = max_slippage
Copy link
Contributor

Choose a reason for hiding this comment

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

If these two terms have the same meaning in this case, can we just stick to using one of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed

Copy link
Contributor

Choose a reason for hiding this comment

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

Hey, there are still uses of the term 'slippage' (in KellyBettingStrategy and the tests)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Renamed.


def _check_price_impact_ok_else_log(
self, buy_direction: bool, bet_size: float, market: AgentMarket
) -> None:
slippage = self.calculate_slippage_for_bet_amount(
buy_direction,
bet_size,
market.outcome_token_pool["Yes"],
market.outcome_token_pool["No"],
0,
)

if slippage > self.max_price_impact and not np.isclose(
slippage, self.max_price_impact, self.max_price_impact / 100
):
logger.info(
f"Slippage {slippage} deviates too much from self.max_slippage {self.max_price_impact}, market_id {market.id}"
Copy link
Contributor

Choose a reason for hiding this comment

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

deviates too much

It sounds pretty bad, yet it's logged as info, and the code continues. Can it be just ignored?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see this happen a lot if I run the script on this branch. If this is ever hit, does that mean there's a bug in calculate_bet_amount_for_price_impact?

Copy link
Contributor Author

@gabrielfior gabrielfior Oct 1, 2024

Choose a reason for hiding this comment

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

Indeed the minimize_scalar was complaining - I adjusted bounds and now it looks better. Please give it a go and let me know if you find issues!
Please note that this is now a ValueError since I want to enforce these boundaries.

)

def calculate_trades(
self,
Expand Down Expand Up @@ -165,9 +193,23 @@ def calculate_trades(
)
)

kelly_bet_size = kelly_bet.size
if self.max_price_impact:
# Adjust amount
max_slippage_bet_amount = self.calculate_bet_amount_for_price_impact(
market, kelly_bet, 0
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Include market fees in price impact calculations

The fee parameter is currently set to 0 when calling calculate_bet_amount_for_price_impact. If the market has a non-zero fee, this could lead to inaccurate calculations. Pass the actual market fee to ensure precise price impact estimation.

Apply this diff to include the market fee:

-max_slippage_bet_amount = self.calculate_bet_amount_for_price_impact(
-    market, kelly_bet, 0
+max_slippage_bet_amount = self.calculate_bet_amount_for_price_impact(
+    market, kelly_bet, fee=market.fee if hasattr(market, 'fee') else 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
max_slippage_bet_amount = self.calculate_bet_amount_for_price_impact(
market, kelly_bet, 0
)
max_slippage_bet_amount = self.calculate_bet_amount_for_price_impact(
market, kelly_bet, fee=market.fee if hasattr(market, 'fee') else 0
)


# We just don't want Kelly size to extrapolate price_impact - hence we take the min.
kelly_bet_size = min(kelly_bet.size, max_slippage_bet_amount)

self._check_price_impact_ok_else_log(
Copy link
Contributor

Choose a reason for hiding this comment

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

So it's possible it's not ok, but it just continues?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please check revised version - now it's a ValueError.

kelly_bet.direction, kelly_bet_size, market
)

amounts = {
market.get_outcome_str_from_bool(kelly_bet.direction): TokenAmount(
amount=kelly_bet.size, currency=market.currency
amount=kelly_bet_size, currency=market.currency
),
}
target_position = Position(market_id=market.id, amounts=amounts)
Expand All @@ -176,13 +218,58 @@ def calculate_trades(
)
return trades

def calculate_slippage_for_bet_amount(
self, buy_direction: bool, bet_amount: float, yes: float, no: float, fee: float
) -> float:
total_outcome_tokens = yes + no
expected_price = (
no / total_outcome_tokens if buy_direction else yes / total_outcome_tokens
)

tokens_bought = get_buy_outcome_token_amount(
bet_amount, buy_direction, yes, no, fee
)

actual_price = bet_amount / tokens_bought
# Slippage should be > 0 if we paid more than expected_price, and
# < 0 if we paid less than expected_price.
slippage = (actual_price - expected_price) / expected_price
return slippage

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(b: xDai) -> float:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this need to be a function within the function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't have to be, it just felt simpler to have this here since it's not needed anywhere else in the class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, that's almost never done in Python. Can you just make it a normal function, please?

Copy link
Contributor

Choose a reason for hiding this comment

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

I dunno, I think it's fine in this context, especially as it's being used as a Callable, passed as an arg to minimize_scalar

ChatGPT agrees:

in python is it good practice to define a function inside a function?
...
If the inner function is being passed as a callable to another function, this can be a valid and clean design choice, especially if the callable depends on the state of the outer function

Copy link
Contributor

Choose a reason for hiding this comment

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

Also I did the same thing here

def f(r: float) -> float:
R = r / (1 - fee)
first_term = other_holdings - R
second_term = holdings + shares_to_sell - R
third_term = holdings * other_holdings
return (first_term * second_term) - third_term
amount_to_sell = newton(f, 0)
😄

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay then, I'm standing overvoted 😄 For sure it's not a blocker.

actual_slippage = self.calculate_slippage_for_bet_amount(
kelly_bet.direction, b, yes_outcome_pool_size, no_outcome_pool_size, fee
)
# translate in y
return abs(actual_slippage - self.max_price_impact)

yes_outcome_pool_size = market.outcome_token_pool[
market.get_outcome_str_from_bool(True)
]
no_outcome_pool_size = market.outcome_token_pool[
market.get_outcome_str_from_bool(False)
]

optimized_bet_amount = minimize_scalar(
calculate_price_impact_deviation_from_target_price_impact,
bounds=(min(yes_outcome_pool_size, no_outcome_pool_size) / 10, 1000),
method="bounded",
tol=1e-7,
options={"maxiter": 10000},
)
return optimized_bet_amount.x

def __repr__(self) -> str:
return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount})"
return f"{self.__class__.__name__}(max_bet_amount={self.max_bet_amount})(max_price_impact={self.max_price_impact})"


class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
def __init__(self, max_bet_amount: float = 10):
def __init__(self, max_bet_amount: float = 10, max_slippage: float | None = None):
self.max_bet_amount = max_bet_amount
self.max_slippage = max_slippage

def adjust_bet_amount(
self, existing_position: Position | None, market: AgentMarket
Expand Down
2 changes: 1 addition & 1 deletion prediction_market_agent_tooling/markets/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class Trade(BaseModel):


class PlacedTrade(Trade):
id: str
id: str | None = None

@staticmethod
def from_trade(trade: Trade, id: str) -> "PlacedTrade":
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ sqlmodel = "^0.0.21"
psycopg2-binary = "^2.9.9"
base58 = ">=1.0.2,<2.0"
loky = "^3.4.1"
hishel = "^0.0.31"

[tool.poetry.extras]
openai = ["openai"]
Expand Down
18 changes: 18 additions & 0 deletions scripts/calc_slippage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
r_a = 10
Copy link
Contributor

Choose a reason for hiding this comment

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

In PMAT, all scripts have been helpful in executing some actions so far.

This feels more for the examples folder. 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

r_b = 10
p_a = r_b / (r_a + r_b)
# alice buys YES (n_a) with b=10
X = 10
n_a = X * (r_a + r_b) / (2 * r_b)
n_b = X * (r_a + r_b) / (2 * r_a)
n_swap = r_a - ((r_a * r_b) / (r_b + ((X * (r_b + r_a)) / (2 * r_a))))
n_r = n_a + n_swap

p_a_new = X / n_r
slippage = (p_a_new - p_a) / p_a
print(f" p_a {p_a} n_swap {n_swap} p_a_new {p_a_new} slippage {slippage}")

# slippage = 0.3333
s = 1 / 5
bet_amount = p_a * (s + 1) * n_r
print(f"bet_amount {bet_amount}")
46 changes: 46 additions & 0 deletions tests/markets/omen/test_kelly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import numpy as np
Copy link
Contributor

Choose a reason for hiding this comment

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

These test are in the omen directory but they don't have anything specific about Omen

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed - moved.


from prediction_market_agent_tooling.deploy.betting_strategy import KellyBettingStrategy


def test_kelly_slippage_calculation1() -> None:
# First case from https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-cpmm
kelly = KellyBettingStrategy(max_bet_amount=1, max_slippage=0.5)
yes = 10
no = 10
bet_amount = 10
buy_direction = True
assert_slip_ok(bet_amount, buy_direction, yes, no, kelly)


def test_kelly_slippage_calculation2() -> None:
kelly = KellyBettingStrategy(max_bet_amount=1, max_slippage=0.5)
# after first bet 10 xDAI on Yes, new yes/no
yes = 5
no = 20
bet_amount = 10
buy_direction = False
assert_slip_ok(bet_amount, buy_direction, yes, no, kelly)


def assert_slip_ok(
bet_amount: float,
buy_direction: bool,
yes: float,
no: float,
kelly: KellyBettingStrategy,
):
# expect
expected_price = yes / (yes + no) # p_yes
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this p_no?

1 - self.outcomeTokenAmounts[self.yes_index] / sum(self.outcomeTokenAmounts)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed yes, no = no, yes if buy_direction is False.
Tests taken from https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-cpmm pass.

tokens_bought = (no + bet_amount) - (
(yes * no) / (yes + bet_amount)
) # 23.333 # x*y = k
actual_price = bet_amount / tokens_bought
expected_slip = (actual_price - expected_price) / expected_price
####
slip = kelly.calculate_slippage_for_bet_amount(
buy_direction, bet_amount=bet_amount, yes=yes, no=no, fee=0
)
assert np.isclose(slip, expected_slip, rtol=0.01)

print(slip)
Loading