prediction-market-agent-tooling 0.65.5__py3-none-any.whl → 0.69.17.dev1149__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
- prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
- prediction_market_agent_tooling/abis/processor.abi.json +16 -0
- prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
- prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
- prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
- prediction_market_agent_tooling/benchmark/utils.py +13 -0
- prediction_market_agent_tooling/chains.py +1 -0
- prediction_market_agent_tooling/config.py +61 -2
- prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
- prediction_market_agent_tooling/deploy/agent.py +199 -67
- prediction_market_agent_tooling/deploy/agent_example.py +1 -1
- prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
- prediction_market_agent_tooling/deploy/constants.py +6 -0
- prediction_market_agent_tooling/gtypes.py +11 -1
- prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
- prediction_market_agent_tooling/loggers.py +9 -1
- prediction_market_agent_tooling/logprobs_parser.py +2 -1
- prediction_market_agent_tooling/markets/agent_market.py +106 -18
- prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
- prediction_market_agent_tooling/markets/data_models.py +120 -7
- prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
- prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
- prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
- prediction_market_agent_tooling/markets/market_type.py +74 -0
- prediction_market_agent_tooling/markets/markets.py +7 -99
- prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
- prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
- prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
- prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
- prediction_market_agent_tooling/markets/omen/omen.py +112 -23
- prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
- prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
- prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
- prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
- prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
- prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
- prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
- prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
- prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
- prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
- prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
- prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
- prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
- prediction_market_agent_tooling/markets/seer/seer.py +393 -106
- prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
- prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
- prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
- prediction_market_agent_tooling/tools/_generic_value.py +8 -2
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
- prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
- prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
- prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
- prediction_market_agent_tooling/tools/contract.py +480 -38
- prediction_market_agent_tooling/tools/contract_utils.py +61 -0
- prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
- prediction_market_agent_tooling/tools/cow/models.py +122 -0
- prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
- prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
- prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
- prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
- prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
- prediction_market_agent_tooling/tools/openai_utils.py +31 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
- prediction_market_agent_tooling/tools/rephrase.py +71 -0
- prediction_market_agent_tooling/tools/singleton.py +11 -6
- prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
- prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
- prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
- prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
- prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
- prediction_market_agent_tooling/tools/utils.py +61 -3
- prediction_market_agent_tooling/tools/web3_utils.py +63 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
- prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,29 +1,68 @@
|
|
|
1
1
|
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
from web3 import Web3
|
|
2
3
|
from web3.constants import ADDRESS_ZERO
|
|
3
4
|
|
|
4
5
|
from prediction_market_agent_tooling.gtypes import (
|
|
6
|
+
ChecksumAddress,
|
|
5
7
|
CollateralToken,
|
|
6
8
|
HexAddress,
|
|
7
9
|
HexBytes,
|
|
8
10
|
OutcomeStr,
|
|
11
|
+
OutcomeToken,
|
|
12
|
+
Wei,
|
|
9
13
|
)
|
|
14
|
+
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
class
|
|
17
|
+
class SwaprToken(BaseModel):
|
|
13
18
|
id: HexBytes
|
|
14
19
|
name: str
|
|
15
20
|
symbol: str
|
|
16
21
|
|
|
22
|
+
@property
|
|
23
|
+
def address(self) -> ChecksumAddress:
|
|
24
|
+
return Web3.to_checksum_address(self.id.hex())
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
|
|
27
|
+
class SwaprPool(BaseModel):
|
|
19
28
|
model_config = ConfigDict(populate_by_name=True)
|
|
20
29
|
id: HexBytes
|
|
21
30
|
liquidity: int
|
|
22
|
-
token0:
|
|
23
|
-
token1:
|
|
31
|
+
token0: SwaprToken
|
|
32
|
+
token1: SwaprToken
|
|
24
33
|
token0Price: CollateralToken
|
|
25
34
|
token1Price: CollateralToken
|
|
26
35
|
sqrtPrice: int
|
|
36
|
+
totalValueLockedToken0: float
|
|
37
|
+
totalValueLockedToken1: float
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SwaprSwap(BaseModel):
|
|
41
|
+
id: str # It's like "0x73afd8f096096552d72a0b40ea66d2076be136c6a531e2f6b190d151a750271e#32" (note the #32) # web3-private-key-ok
|
|
42
|
+
recipient: HexAddress
|
|
43
|
+
sender: HexAddress
|
|
44
|
+
price: Wei
|
|
45
|
+
amount0: CollateralToken
|
|
46
|
+
amount1: CollateralToken
|
|
47
|
+
token0: SwaprToken
|
|
48
|
+
token1: SwaprToken
|
|
49
|
+
timestamp: int
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def timestamp_utc(self) -> DatetimeUTC:
|
|
53
|
+
return DatetimeUTC.to_datetime_utc(self.timestamp)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def added_to_pool(self) -> CollateralToken:
|
|
57
|
+
return self.amount0 if self.amount0 > 0 else self.amount1
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def withdrawn_from_pool(self) -> OutcomeToken:
|
|
61
|
+
return (
|
|
62
|
+
OutcomeToken(abs(self.amount0).value)
|
|
63
|
+
if self.amount0 < 0
|
|
64
|
+
else OutcomeToken(abs(self.amount1).value)
|
|
65
|
+
)
|
|
27
66
|
|
|
28
67
|
|
|
29
68
|
class NewMarketEvent(BaseModel):
|
|
@@ -58,7 +97,3 @@ class CreateCategoricalMarketsParams(BaseModel):
|
|
|
58
97
|
) # typed as int for later .model_dump() usage (if using Wei, other keys also exported)
|
|
59
98
|
opening_time: int = Field(..., alias="openingTime")
|
|
60
99
|
token_names: list[str] = Field(..., alias="tokenNames")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class SeerParentMarket(BaseModel):
|
|
64
|
-
id: HexBytes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from web3 import Web3
|
|
2
|
+
|
|
3
|
+
from prediction_market_agent_tooling.config import APIKeys
|
|
4
|
+
from prediction_market_agent_tooling.gtypes import (
|
|
5
|
+
ChecksumAddress,
|
|
6
|
+
HexBytes,
|
|
7
|
+
HexStr,
|
|
8
|
+
TxReceipt,
|
|
9
|
+
Wei,
|
|
10
|
+
)
|
|
11
|
+
from prediction_market_agent_tooling.markets.seer.data_models import (
|
|
12
|
+
ExactInputSingleParams,
|
|
13
|
+
)
|
|
14
|
+
from prediction_market_agent_tooling.markets.seer.price_manager import PriceManager
|
|
15
|
+
from prediction_market_agent_tooling.markets.seer.seer_contracts import (
|
|
16
|
+
SwaprRouterContract,
|
|
17
|
+
)
|
|
18
|
+
from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
|
|
19
|
+
SeerSubgraphHandler,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SwapPoolHandler:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
api_keys: APIKeys,
|
|
27
|
+
market_id: str,
|
|
28
|
+
collateral_token_address: ChecksumAddress,
|
|
29
|
+
seer_subgraph: SeerSubgraphHandler | None = None,
|
|
30
|
+
):
|
|
31
|
+
self.api_keys = api_keys
|
|
32
|
+
self.market_id = market_id
|
|
33
|
+
self.collateral_token_address = collateral_token_address
|
|
34
|
+
self.seer_subgraph = seer_subgraph or SeerSubgraphHandler()
|
|
35
|
+
|
|
36
|
+
def _calculate_amount_out_minimum(
|
|
37
|
+
self,
|
|
38
|
+
amount_in: Wei,
|
|
39
|
+
token_in: ChecksumAddress,
|
|
40
|
+
token_out: ChecksumAddress,
|
|
41
|
+
buffer_pct: float = 0.05,
|
|
42
|
+
) -> Wei:
|
|
43
|
+
price_manager = PriceManager.build(HexBytes(HexStr(self.market_id)))
|
|
44
|
+
value = price_manager.get_swapr_input_quote(
|
|
45
|
+
input_amount=amount_in, input_token=token_in, output_token=token_out
|
|
46
|
+
)
|
|
47
|
+
return value * (1 - buffer_pct)
|
|
48
|
+
|
|
49
|
+
def buy_or_sell_outcome_token(
|
|
50
|
+
self,
|
|
51
|
+
amount_in: Wei,
|
|
52
|
+
token_in: ChecksumAddress,
|
|
53
|
+
token_out: ChecksumAddress,
|
|
54
|
+
web3: Web3 | None = None,
|
|
55
|
+
) -> TxReceipt:
|
|
56
|
+
"""Buys/sells outcome_tokens in exchange for collateral tokens"""
|
|
57
|
+
if self.collateral_token_address not in [token_in, token_out]:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"trading outcome_token for a token different than collateral_token {self.collateral_token_address} is not supported. {token_in=} {token_out=}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
amount_out_minimum = self._calculate_amount_out_minimum(
|
|
63
|
+
amount_in=amount_in,
|
|
64
|
+
token_in=token_in,
|
|
65
|
+
token_out=token_out,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
p = ExactInputSingleParams(
|
|
69
|
+
token_in=token_in,
|
|
70
|
+
token_out=token_out,
|
|
71
|
+
recipient=self.api_keys.bet_from_address,
|
|
72
|
+
amount_in=amount_in,
|
|
73
|
+
amount_out_minimum=amount_out_minimum,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
tx_receipt = SwaprRouterContract().exact_input_single(
|
|
77
|
+
api_keys=self.api_keys, params=p, web3=web3
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return tx_receipt
|
|
@@ -60,6 +60,10 @@ class _GenericValue(
|
|
|
60
60
|
cls.parser = parser
|
|
61
61
|
|
|
62
62
|
def __init__(self, value: InputValueType) -> None:
|
|
63
|
+
if isinstance(value, str) and (
|
|
64
|
+
value.startswith("0x") or value.startswith("-0x")
|
|
65
|
+
):
|
|
66
|
+
value = int(value, 16) # type: ignore[assignment] # mypy is confused but if `parser` below can process it, it should be fine, and if it cannot, it should be catched by mypy earlier than here.
|
|
63
67
|
self.value: InternalValueType = self.parser(value)
|
|
64
68
|
super().__init__({"value": self.value, "type": self.__class__.__name__})
|
|
65
69
|
|
|
@@ -187,9 +191,11 @@ class _GenericValue(
|
|
|
187
191
|
raise TypeError("Cannot compare different types")
|
|
188
192
|
return bool(self.value >= other.value)
|
|
189
193
|
|
|
190
|
-
def __eq__(self: GenericValueType, other: GenericValueType | t.Literal[0]) -> bool: # type: ignore
|
|
194
|
+
def __eq__(self: GenericValueType, other: GenericValueType | t.Literal[0] | None) -> bool: # type: ignore
|
|
191
195
|
if other == 0:
|
|
192
196
|
other = self.zero()
|
|
197
|
+
if other is None:
|
|
198
|
+
return False
|
|
193
199
|
if not isinstance(other, _GenericValue):
|
|
194
200
|
raise TypeError("Cannot compare different types")
|
|
195
201
|
if type(self) is not type(other):
|
|
@@ -239,7 +245,7 @@ class _GenericValue(
|
|
|
239
245
|
cls, source_type: t.Any, handler: GetCoreSchemaHandler
|
|
240
246
|
) -> CoreSchema:
|
|
241
247
|
# Support for Pydantic usage.
|
|
242
|
-
dt_schema = handler(
|
|
248
|
+
dt_schema = handler(cls.parser | str | dict)
|
|
243
249
|
return core_schema.no_info_after_validator_function(
|
|
244
250
|
lambda x: cls(x["value"] if isinstance(x, dict) else x),
|
|
245
251
|
dt_schema,
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
from
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from itertools import chain
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy.optimize import minimize
|
|
7
|
+
|
|
8
|
+
from prediction_market_agent_tooling.gtypes import (
|
|
9
|
+
CollateralToken,
|
|
10
|
+
OutcomeToken,
|
|
11
|
+
Probability,
|
|
12
|
+
)
|
|
13
|
+
from prediction_market_agent_tooling.loggers import logger
|
|
2
14
|
from prediction_market_agent_tooling.markets.market_fees import MarketFees
|
|
3
|
-
from prediction_market_agent_tooling.tools.betting_strategies.utils import
|
|
15
|
+
from prediction_market_agent_tooling.tools.betting_strategies.utils import (
|
|
16
|
+
BinaryKellyBet,
|
|
17
|
+
CategoricalKellyBet,
|
|
18
|
+
)
|
|
19
|
+
from prediction_market_agent_tooling.tools.utils import check_not_none
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class KellyType(str, Enum):
|
|
23
|
+
SIMPLE = "simple"
|
|
24
|
+
FULL = "full"
|
|
4
25
|
|
|
5
26
|
|
|
6
27
|
def check_is_valid_probability(probability: float) -> None:
|
|
@@ -13,7 +34,7 @@ def get_kelly_bet_simplified(
|
|
|
13
34
|
market_p_yes: float,
|
|
14
35
|
estimated_p_yes: float,
|
|
15
36
|
confidence: float,
|
|
16
|
-
) ->
|
|
37
|
+
) -> BinaryKellyBet:
|
|
17
38
|
"""
|
|
18
39
|
Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
|
|
19
40
|
|
|
@@ -39,22 +60,24 @@ def get_kelly_bet_simplified(
|
|
|
39
60
|
if estimated_p_yes > market_p_yes:
|
|
40
61
|
bet_direction = True
|
|
41
62
|
market_prob = market_p_yes
|
|
63
|
+
estimated_p = estimated_p_yes
|
|
42
64
|
else:
|
|
43
65
|
bet_direction = False
|
|
44
66
|
market_prob = 1 - market_p_yes
|
|
67
|
+
estimated_p = 1 - estimated_p_yes
|
|
45
68
|
|
|
46
69
|
# Handle the case where market_prob is 0
|
|
47
70
|
if market_prob == 0:
|
|
48
71
|
market_prob = 1e-10
|
|
49
72
|
|
|
50
|
-
edge = abs(
|
|
73
|
+
edge = abs(estimated_p - market_prob) * confidence
|
|
51
74
|
odds = (1 / market_prob) - 1
|
|
52
75
|
kelly_fraction = edge / odds
|
|
53
76
|
|
|
54
77
|
# Ensure bet size is non-negative does not exceed the wallet balance
|
|
55
78
|
bet_size = CollateralToken(min(kelly_fraction * max_bet.value, max_bet.value))
|
|
56
79
|
|
|
57
|
-
return
|
|
80
|
+
return BinaryKellyBet(direction=bet_direction, size=bet_size)
|
|
58
81
|
|
|
59
82
|
|
|
60
83
|
def get_kelly_bet_full(
|
|
@@ -64,7 +87,7 @@ def get_kelly_bet_full(
|
|
|
64
87
|
confidence: float,
|
|
65
88
|
max_bet: CollateralToken,
|
|
66
89
|
fees: MarketFees,
|
|
67
|
-
) ->
|
|
90
|
+
) -> BinaryKellyBet:
|
|
68
91
|
"""
|
|
69
92
|
Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
|
|
70
93
|
|
|
@@ -98,7 +121,7 @@ def get_kelly_bet_full(
|
|
|
98
121
|
check_is_valid_probability(confidence)
|
|
99
122
|
|
|
100
123
|
if max_bet == 0:
|
|
101
|
-
return
|
|
124
|
+
return BinaryKellyBet(size=CollateralToken(0), direction=True)
|
|
102
125
|
|
|
103
126
|
x = yes_outcome_pool_size.value
|
|
104
127
|
y = no_outcome_pool_size.value
|
|
@@ -144,7 +167,247 @@ def get_kelly_bet_full(
|
|
|
144
167
|
kelly_bet_amount = numerator / denominator
|
|
145
168
|
|
|
146
169
|
# Clip the bet size to max_bet to account for rounding errors.
|
|
147
|
-
return
|
|
170
|
+
return BinaryKellyBet(
|
|
148
171
|
direction=kelly_bet_amount > 0,
|
|
149
172
|
size=CollateralToken(min(max_bet.value, abs(kelly_bet_amount))),
|
|
150
173
|
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_kelly_bets_categorical_simplified(
|
|
177
|
+
market_probabilities: list[Probability],
|
|
178
|
+
estimated_probabilities: list[Probability],
|
|
179
|
+
confidence: float,
|
|
180
|
+
max_bet: CollateralToken,
|
|
181
|
+
fees: MarketFees,
|
|
182
|
+
allow_multiple_bets: bool,
|
|
183
|
+
allow_shorting: bool,
|
|
184
|
+
bet_precision: int = 6,
|
|
185
|
+
) -> list[CategoricalKellyBet]:
|
|
186
|
+
"""
|
|
187
|
+
Calculate Kelly bets for categorical markets using only market probabilities.
|
|
188
|
+
Returns a list of CategoricalKellyBet objects, one for each outcome.
|
|
189
|
+
Considers max_bet across all outcomes together.
|
|
190
|
+
Indicates both buying (long) and shorting (selling) by allowing negative bet sizes.
|
|
191
|
+
"""
|
|
192
|
+
for p in chain(market_probabilities, estimated_probabilities, [confidence]):
|
|
193
|
+
check_is_valid_probability(p)
|
|
194
|
+
assert len(market_probabilities) == len(
|
|
195
|
+
estimated_probabilities
|
|
196
|
+
), "Mismatch in number of outcomes"
|
|
197
|
+
|
|
198
|
+
f = 1 - fees.bet_proportion
|
|
199
|
+
|
|
200
|
+
total_kelly_fraction = 0.0
|
|
201
|
+
kelly_fractions = []
|
|
202
|
+
|
|
203
|
+
for i in range(len(market_probabilities)):
|
|
204
|
+
estimated_p = estimated_probabilities[i]
|
|
205
|
+
market_p = max(market_probabilities[i], 1e-10)
|
|
206
|
+
|
|
207
|
+
edge = (estimated_p - market_p) * confidence
|
|
208
|
+
odds = (1 / market_p) - 1
|
|
209
|
+
kelly_fraction = edge / odds * f
|
|
210
|
+
|
|
211
|
+
if not allow_shorting:
|
|
212
|
+
kelly_fraction = max(0, kelly_fraction)
|
|
213
|
+
|
|
214
|
+
kelly_fractions.append(kelly_fraction)
|
|
215
|
+
total_kelly_fraction += abs(kelly_fraction)
|
|
216
|
+
|
|
217
|
+
best_kelly_fraction_index = max(
|
|
218
|
+
range(len(kelly_fractions)), key=lambda i: abs(kelly_fractions[i])
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
bets = []
|
|
222
|
+
for i, kelly_fraction in enumerate(kelly_fractions):
|
|
223
|
+
if not allow_multiple_bets:
|
|
224
|
+
bet_size = (
|
|
225
|
+
kelly_fraction * max_bet.value if i == best_kelly_fraction_index else 0
|
|
226
|
+
)
|
|
227
|
+
elif allow_multiple_bets and total_kelly_fraction > 0:
|
|
228
|
+
bet_size = (kelly_fraction / total_kelly_fraction) * max_bet.value
|
|
229
|
+
else:
|
|
230
|
+
bet_size = 0.0
|
|
231
|
+
# Ensure bet_size is within [-max_bet.value, max_bet.value]
|
|
232
|
+
bet_size = max(-max_bet.value, min(bet_size, max_bet.value))
|
|
233
|
+
bets.append(
|
|
234
|
+
CategoricalKellyBet(
|
|
235
|
+
index=i, size=CollateralToken(round(bet_size, bet_precision))
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return bets
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_kelly_bets_categorical_full(
|
|
243
|
+
market_probabilities: list[Probability],
|
|
244
|
+
estimated_probabilities: list[Probability],
|
|
245
|
+
confidence: float,
|
|
246
|
+
max_bet: CollateralToken,
|
|
247
|
+
fees: MarketFees,
|
|
248
|
+
allow_multiple_bets: bool,
|
|
249
|
+
allow_shorting: bool,
|
|
250
|
+
multicategorical: bool,
|
|
251
|
+
# investment amount, outcome index --> received outcome tokens
|
|
252
|
+
get_buy_token_amount: Callable[[CollateralToken, int], OutcomeToken],
|
|
253
|
+
bet_precision: int = 6,
|
|
254
|
+
) -> list[CategoricalKellyBet]:
|
|
255
|
+
"""
|
|
256
|
+
Calculate Kelly bets for categorical markets using joint optimization over all outcomes,
|
|
257
|
+
splitting the max bet between all possible outcomes to maximize expected log utility.
|
|
258
|
+
Returns a list of CategoricalKellyBet objects, one for each outcome.
|
|
259
|
+
Handles both buying (long) and shorting (selling) by allowing negative bet sizes.
|
|
260
|
+
If the agent's probabilities are very close to the market's, returns all-zero bets.
|
|
261
|
+
multicategorical means that multiple outcomes could be selected as correct ones.
|
|
262
|
+
"""
|
|
263
|
+
assert len(market_probabilities) == len(
|
|
264
|
+
estimated_probabilities
|
|
265
|
+
), "Mismatch in number of outcomes"
|
|
266
|
+
for p in chain(market_probabilities, estimated_probabilities, [confidence]):
|
|
267
|
+
check_is_valid_probability(p)
|
|
268
|
+
|
|
269
|
+
n = len(market_probabilities)
|
|
270
|
+
max_bet_value = max_bet.value
|
|
271
|
+
|
|
272
|
+
if all(
|
|
273
|
+
abs(estimated_probabilities[i] - market_probabilities[i]) < 1e-3
|
|
274
|
+
for i in range(n)
|
|
275
|
+
):
|
|
276
|
+
return [
|
|
277
|
+
CategoricalKellyBet(index=i, size=CollateralToken(0.0)) for i in range(n)
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
def compute_payouts(bets: list[float]) -> list[float]:
|
|
281
|
+
payouts: list[float] = []
|
|
282
|
+
for i in range(n):
|
|
283
|
+
payout = 0.0
|
|
284
|
+
if bets[i] >= 0:
|
|
285
|
+
# If bet on i is positive, we buy outcome i
|
|
286
|
+
buy_result = get_buy_token_amount(CollateralToken(bets[i]), i)
|
|
287
|
+
payout += buy_result.value
|
|
288
|
+
else:
|
|
289
|
+
# If bet is negative, we "short" outcome i by buying all other outcomes
|
|
290
|
+
for j in range(n):
|
|
291
|
+
if j == i:
|
|
292
|
+
continue
|
|
293
|
+
buy_result = get_buy_token_amount(
|
|
294
|
+
CollateralToken(abs(bets[i]) / (n - 1)), j
|
|
295
|
+
)
|
|
296
|
+
payout += buy_result.value
|
|
297
|
+
payouts.append(payout)
|
|
298
|
+
return payouts
|
|
299
|
+
|
|
300
|
+
def adjust_prob(my_prob: float, market_prob: float) -> float:
|
|
301
|
+
# Based on the confidence, shrinks the predicted probability towards market's current probability.
|
|
302
|
+
return confidence * my_prob + (1 - confidence) * market_prob
|
|
303
|
+
|
|
304
|
+
# Use the simple version to estimate the initial bet vector.
|
|
305
|
+
x0 = np.array(
|
|
306
|
+
[
|
|
307
|
+
x.size.value # Use simplified value as starting point
|
|
308
|
+
for x in get_kelly_bets_categorical_simplified(
|
|
309
|
+
market_probabilities=market_probabilities,
|
|
310
|
+
estimated_probabilities=estimated_probabilities,
|
|
311
|
+
confidence=confidence,
|
|
312
|
+
max_bet=max_bet,
|
|
313
|
+
fees=fees,
|
|
314
|
+
allow_multiple_bets=allow_multiple_bets,
|
|
315
|
+
allow_shorting=allow_shorting,
|
|
316
|
+
bet_precision=bet_precision,
|
|
317
|
+
)
|
|
318
|
+
]
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Track the best solution found during optimization
|
|
322
|
+
best_solution_bets = None
|
|
323
|
+
best_solution_utility = float("-inf")
|
|
324
|
+
|
|
325
|
+
def neg_expected_log_utility(bets: list[float]) -> float:
|
|
326
|
+
"""
|
|
327
|
+
Negative expected log utility for categorical Kelly betting.
|
|
328
|
+
This function is minimized to find the optimal bet allocation.
|
|
329
|
+
"""
|
|
330
|
+
adj_probs = [
|
|
331
|
+
adjust_prob(estimated_probabilities[i], market_probabilities[i])
|
|
332
|
+
for i in range(n)
|
|
333
|
+
]
|
|
334
|
+
payouts = compute_payouts(bets)
|
|
335
|
+
|
|
336
|
+
profits = [payout - abs(bet) for payout, bet in zip(payouts, bets)]
|
|
337
|
+
|
|
338
|
+
# Ensure profits are not too negative to avoid log(negative) or log(0)
|
|
339
|
+
# Use a small epsilon to prevent numerical instability
|
|
340
|
+
min_profit = -0.99 # Ensure 1 + profit > 0.01
|
|
341
|
+
profits = [max(profit, min_profit) for profit in profits]
|
|
342
|
+
|
|
343
|
+
# Expected log utility
|
|
344
|
+
expected_log_utility: float = sum(
|
|
345
|
+
adj_probs[i] * np.log(1 + profits[i]) for i in range(n)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Track the best solution found so far
|
|
349
|
+
nonlocal best_solution_bets, best_solution_utility
|
|
350
|
+
if expected_log_utility > best_solution_utility:
|
|
351
|
+
best_solution_bets = np.array(bets)
|
|
352
|
+
best_solution_utility = expected_log_utility
|
|
353
|
+
|
|
354
|
+
# Return negative for minimization
|
|
355
|
+
return -expected_log_utility
|
|
356
|
+
|
|
357
|
+
constraints = [
|
|
358
|
+
# We can not bet more than `max_bet_value`
|
|
359
|
+
{
|
|
360
|
+
"type": "ineq",
|
|
361
|
+
"fun": lambda bets: max_bet_value - np.sum(np.abs(bets)),
|
|
362
|
+
},
|
|
363
|
+
# Each bet should not result in guaranteed loss
|
|
364
|
+
{
|
|
365
|
+
"type": "ineq",
|
|
366
|
+
"fun": lambda bets: [
|
|
367
|
+
payout
|
|
368
|
+
- (sum(abs(b) for b in bets) if not multicategorical else abs(bets[i]))
|
|
369
|
+
for i, payout in enumerate(compute_payouts(bets))
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
result = minimize(
|
|
375
|
+
neg_expected_log_utility,
|
|
376
|
+
x0,
|
|
377
|
+
method="SLSQP",
|
|
378
|
+
bounds=[
|
|
379
|
+
((-max_bet_value if allow_shorting else 0), max_bet_value) for _ in range(n)
|
|
380
|
+
],
|
|
381
|
+
constraints=constraints,
|
|
382
|
+
options={"maxiter": 10_000},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# This can sometimes happen, as long as it's occasional, it's should be fine to just use simplified version approximation.
|
|
386
|
+
if not result.success:
|
|
387
|
+
logger.warning(
|
|
388
|
+
f"Joint optimization failed: {result=} {x0=} {estimated_probabilities=} {confidence=} {market_probabilities=}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Use the best solution found during optimization, not just the final result (result.x).
|
|
392
|
+
# This is important because SLSQP may end on a worse solution due to numerical issues.
|
|
393
|
+
bet_vector = check_not_none(best_solution_bets) if result.success else x0
|
|
394
|
+
|
|
395
|
+
if not allow_multiple_bets:
|
|
396
|
+
# If we are not allowing multiple bets, we need to ensure only one bet is non-zero.
|
|
397
|
+
# We can do this by taking the maximum bet and setting all others to zero.
|
|
398
|
+
# We do this, instead of enforcing it in with additional constraint,
|
|
399
|
+
# because such hard constraint is problematic for the solver and results in almost always failing to optimize.
|
|
400
|
+
max_bet_index = np.argmax(np.abs(bet_vector))
|
|
401
|
+
max_bet_value = bet_vector[max_bet_index]
|
|
402
|
+
|
|
403
|
+
bet_vector = np.zeros_like(bet_vector)
|
|
404
|
+
bet_vector[max_bet_index] = max_bet_value
|
|
405
|
+
|
|
406
|
+
bets = [
|
|
407
|
+
CategoricalKellyBet(
|
|
408
|
+
index=i, size=CollateralToken(round(bet_vector[i], bet_precision))
|
|
409
|
+
)
|
|
410
|
+
for i in range(n)
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
return bets
|
|
@@ -3,6 +3,11 @@ from pydantic import BaseModel
|
|
|
3
3
|
from prediction_market_agent_tooling.gtypes import CollateralToken
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class
|
|
6
|
+
class BinaryKellyBet(BaseModel):
|
|
7
7
|
direction: bool
|
|
8
8
|
size: CollateralToken
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CategoricalKellyBet(BaseModel):
|
|
12
|
+
index: int
|
|
13
|
+
size: CollateralToken
|