prediction-market-agent-tooling 0.67.3__py3-none-any.whl → 0.67.4.dev992__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/deploy/agent.py +2 -4
- prediction_market_agent_tooling/deploy/betting_strategy.py +169 -49
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +2 -2
- prediction_market_agent_tooling/markets/agent_market.py +17 -0
- prediction_market_agent_tooling/markets/blockchain_utils.py +5 -3
- prediction_market_agent_tooling/markets/data_models.py +23 -3
- prediction_market_agent_tooling/markets/omen/data_models.py +6 -1
- prediction_market_agent_tooling/markets/omen/omen.py +43 -6
- prediction_market_agent_tooling/markets/polymarket/api.py +9 -3
- prediction_market_agent_tooling/markets/polymarket/data_models.py +5 -3
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +15 -7
- prediction_market_agent_tooling/markets/seer/seer.py +11 -2
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +4 -1
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +276 -8
- prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
- prediction_market_agent_tooling/tools/hexbytes_custom.py +9 -0
- prediction_market_agent_tooling/tools/httpx_cached_client.py +5 -3
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +4 -3
- prediction_market_agent_tooling/tools/singleton.py +11 -6
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/METADATA +1 -1
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/RECORD +24 -24
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/LICENSE +0 -0
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/WHEEL +0 -0
- {prediction_market_agent_tooling-0.67.3.dist-info → prediction_market_agent_tooling-0.67.4.dev992.dist-info}/entry_points.txt +0 -0
@@ -59,16 +59,18 @@ class PolymarketGammaResponseDataItem(BaseModel):
|
|
59
59
|
id: str
|
60
60
|
slug: str
|
61
61
|
volume: float | None = None
|
62
|
-
startDate: DatetimeUTC
|
62
|
+
startDate: DatetimeUTC | None = None
|
63
63
|
endDate: DatetimeUTC | None = None
|
64
64
|
liquidity: float | None = None
|
65
65
|
liquidityClob: float | None = None
|
66
66
|
title: str
|
67
|
-
description: str
|
67
|
+
description: str | None = None
|
68
68
|
archived: bool
|
69
69
|
closed: bool
|
70
70
|
active: bool
|
71
|
-
markets: list[
|
71
|
+
markets: list[
|
72
|
+
PolymarketGammaMarket
|
73
|
+
] | None = None # Some Polymarket markets have missing markets field. We skip these markets manually when retrieving.
|
72
74
|
tags: list[PolymarketGammaTag]
|
73
75
|
|
74
76
|
@property
|
@@ -7,6 +7,7 @@ from prediction_market_agent_tooling.gtypes import (
|
|
7
7
|
OutcomeStr,
|
8
8
|
Probability,
|
9
9
|
)
|
10
|
+
from prediction_market_agent_tooling.loggers import logger
|
10
11
|
from prediction_market_agent_tooling.markets.agent_market import (
|
11
12
|
AgentMarket,
|
12
13
|
ConditionalFilterType,
|
@@ -31,6 +32,7 @@ from prediction_market_agent_tooling.markets.polymarket.polymarket_subgraph_hand
|
|
31
32
|
PolymarketSubgraphHandler,
|
32
33
|
)
|
33
34
|
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
35
|
+
from prediction_market_agent_tooling.tools.utils import check_not_none
|
34
36
|
|
35
37
|
|
36
38
|
class PolymarketAgentMarket(AgentMarket):
|
@@ -69,9 +71,11 @@ class PolymarketAgentMarket(AgentMarket):
|
|
69
71
|
]
|
70
72
|
# For a binary market, there should be exactly one payout numerator greater than 0.
|
71
73
|
if len(payout_numerator_indices_gt_0) != 1:
|
72
|
-
|
73
|
-
|
74
|
+
# These cases involve multi-categorical resolution (to be implemented https://github.com/gnosis/prediction-market-agent-tooling/issues/770)
|
75
|
+
logger.warning(
|
76
|
+
f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators} for condition_id {condition_id.hex()}"
|
74
77
|
)
|
78
|
+
return Resolution(outcome=None, invalid=False)
|
75
79
|
|
76
80
|
# we return the only payout numerator greater than 0 as resolution
|
77
81
|
resolved_outcome = outcomes[payout_numerator_indices_gt_0[0]]
|
@@ -83,16 +87,16 @@ class PolymarketAgentMarket(AgentMarket):
|
|
83
87
|
condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
|
84
88
|
) -> "PolymarketAgentMarket":
|
85
89
|
# If len(model.markets) > 0, this denotes a categorical market.
|
86
|
-
|
87
|
-
outcomes =
|
88
|
-
outcome_prices =
|
90
|
+
markets = check_not_none(model.markets)
|
91
|
+
outcomes = markets[0].outcomes_list
|
92
|
+
outcome_prices = markets[0].outcome_prices
|
89
93
|
if not outcome_prices:
|
90
94
|
# We give random prices
|
91
95
|
outcome_prices = [0.5, 0.5]
|
92
96
|
probabilities = {o: Probability(op) for o, op in zip(outcomes, outcome_prices)}
|
93
97
|
|
94
98
|
resolution = PolymarketAgentMarket.build_resolution_from_condition(
|
95
|
-
condition_id=
|
99
|
+
condition_id=markets[0].conditionId,
|
96
100
|
condition_model_dict=condition_model_dict,
|
97
101
|
outcomes=outcomes,
|
98
102
|
)
|
@@ -164,7 +168,11 @@ class PolymarketAgentMarket(AgentMarket):
|
|
164
168
|
)
|
165
169
|
|
166
170
|
condition_models = PolymarketSubgraphHandler().get_conditions(
|
167
|
-
condition_ids=[
|
171
|
+
condition_ids=[
|
172
|
+
market.markets[0].conditionId
|
173
|
+
for market in markets
|
174
|
+
if market.markets is not None
|
175
|
+
]
|
168
176
|
)
|
169
177
|
condition_models_dict = {c.id: c for c in condition_models}
|
170
178
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
2
2
|
import typing as t
|
3
3
|
from datetime import timedelta
|
4
4
|
|
5
|
+
import cachetools
|
5
6
|
from cowdao_cowpy.common.api.errors import UnexpectedResponseError
|
6
7
|
from eth_typing import ChecksumAddress
|
7
8
|
from web3 import Web3
|
@@ -99,6 +100,11 @@ from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow
|
|
99
100
|
SEER_TINY_BET_AMOUNT = USD(0.1)
|
100
101
|
|
101
102
|
|
103
|
+
SHARED_CACHE: cachetools.TTLCache[t.Hashable, t.Any] = cachetools.TTLCache(
|
104
|
+
maxsize=256, ttl=10 * 60
|
105
|
+
)
|
106
|
+
|
107
|
+
|
102
108
|
class SeerAgentMarket(AgentMarket):
|
103
109
|
wrapped_tokens: list[ChecksumAddress]
|
104
110
|
creator: HexAddress
|
@@ -541,6 +547,7 @@ class SeerAgentMarket(AgentMarket):
|
|
541
547
|
liquidity = self.get_liquidity_for_outcome(outcome)
|
542
548
|
return liquidity > self.minimum_market_liquidity_required
|
543
549
|
|
550
|
+
@cachetools.cached(cache=SHARED_CACHE, key=lambda self: f"has_liquidity_{self.id}")
|
544
551
|
def has_liquidity(self) -> bool:
|
545
552
|
# We define a market as having liquidity if it has liquidity for all outcomes except for the invalid (index -1)
|
546
553
|
return all(
|
@@ -594,7 +601,7 @@ class SeerAgentMarket(AgentMarket):
|
|
594
601
|
f"Expected exactly 1 trade from {order_metadata=}, but got {len(trades)=}."
|
595
602
|
)
|
596
603
|
cow_tx_hash = trades[0].txHash
|
597
|
-
logger.info(f"TxHash for {order_metadata.uid.root=}
|
604
|
+
logger.info(f"TxHash is {cow_tx_hash=} for {order_metadata.uid.root=}.")
|
598
605
|
return cow_tx_hash.hex()
|
599
606
|
|
600
607
|
except (
|
@@ -626,7 +633,9 @@ class SeerAgentMarket(AgentMarket):
|
|
626
633
|
amount_wei=amount_wei,
|
627
634
|
web3=web3,
|
628
635
|
)
|
629
|
-
|
636
|
+
swap_pool_tx_hash = tx_receipt["transactionHash"].hex()
|
637
|
+
logger.info(f"TxHash is {swap_pool_tx_hash=}.")
|
638
|
+
return swap_pool_tx_hash
|
630
639
|
|
631
640
|
def place_bet(
|
632
641
|
self,
|
@@ -71,7 +71,10 @@ class SwapPoolHandler:
|
|
71
71
|
price_outcome_token = PriceManager.build(
|
72
72
|
HexBytes(HexStr(self.market_id))
|
73
73
|
).get_token_price_from_pools(token=outcome_token)
|
74
|
-
if
|
74
|
+
if (
|
75
|
+
not price_outcome_token
|
76
|
+
or not price_outcome_token.priceOfCollateralInAskingToken
|
77
|
+
):
|
75
78
|
raise ValueError(
|
76
79
|
f"Could not find price for {outcome_token=} and {self.collateral_token_address}"
|
77
80
|
)
|
@@ -1,6 +1,24 @@
|
|
1
|
-
from
|
1
|
+
from itertools import chain
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
from scipy.optimize import minimize
|
5
|
+
|
6
|
+
from prediction_market_agent_tooling.gtypes import (
|
7
|
+
CollateralToken,
|
8
|
+
OutcomeToken,
|
9
|
+
Probability,
|
10
|
+
)
|
11
|
+
from prediction_market_agent_tooling.loggers import logger
|
12
|
+
from prediction_market_agent_tooling.markets.agent_market import AgentMarket
|
2
13
|
from prediction_market_agent_tooling.markets.market_fees import MarketFees
|
3
|
-
from prediction_market_agent_tooling.
|
14
|
+
from prediction_market_agent_tooling.markets.omen.omen import (
|
15
|
+
calculate_buy_outcome_token,
|
16
|
+
)
|
17
|
+
from prediction_market_agent_tooling.tools.betting_strategies.utils import (
|
18
|
+
BinaryKellyBet,
|
19
|
+
CategoricalKellyBet,
|
20
|
+
)
|
21
|
+
from prediction_market_agent_tooling.tools.utils import check_not_none
|
4
22
|
|
5
23
|
|
6
24
|
def check_is_valid_probability(probability: float) -> None:
|
@@ -13,7 +31,7 @@ def get_kelly_bet_simplified(
|
|
13
31
|
market_p_yes: float,
|
14
32
|
estimated_p_yes: float,
|
15
33
|
confidence: float,
|
16
|
-
) ->
|
34
|
+
) -> BinaryKellyBet:
|
17
35
|
"""
|
18
36
|
Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
|
19
37
|
|
@@ -39,22 +57,24 @@ def get_kelly_bet_simplified(
|
|
39
57
|
if estimated_p_yes > market_p_yes:
|
40
58
|
bet_direction = True
|
41
59
|
market_prob = market_p_yes
|
60
|
+
estimated_p = estimated_p_yes
|
42
61
|
else:
|
43
62
|
bet_direction = False
|
44
63
|
market_prob = 1 - market_p_yes
|
64
|
+
estimated_p = 1 - estimated_p_yes
|
45
65
|
|
46
66
|
# Handle the case where market_prob is 0
|
47
67
|
if market_prob == 0:
|
48
68
|
market_prob = 1e-10
|
49
69
|
|
50
|
-
edge = abs(
|
70
|
+
edge = abs(estimated_p - market_prob) * confidence
|
51
71
|
odds = (1 / market_prob) - 1
|
52
72
|
kelly_fraction = edge / odds
|
53
73
|
|
54
74
|
# Ensure bet size is non-negative does not exceed the wallet balance
|
55
75
|
bet_size = CollateralToken(min(kelly_fraction * max_bet.value, max_bet.value))
|
56
76
|
|
57
|
-
return
|
77
|
+
return BinaryKellyBet(direction=bet_direction, size=bet_size)
|
58
78
|
|
59
79
|
|
60
80
|
def get_kelly_bet_full(
|
@@ -64,7 +84,7 @@ def get_kelly_bet_full(
|
|
64
84
|
confidence: float,
|
65
85
|
max_bet: CollateralToken,
|
66
86
|
fees: MarketFees,
|
67
|
-
) ->
|
87
|
+
) -> BinaryKellyBet:
|
68
88
|
"""
|
69
89
|
Calculate the optimal bet amount using the Kelly Criterion for a binary outcome market.
|
70
90
|
|
@@ -98,7 +118,7 @@ def get_kelly_bet_full(
|
|
98
118
|
check_is_valid_probability(confidence)
|
99
119
|
|
100
120
|
if max_bet == 0:
|
101
|
-
return
|
121
|
+
return BinaryKellyBet(size=CollateralToken(0), direction=True)
|
102
122
|
|
103
123
|
x = yes_outcome_pool_size.value
|
104
124
|
y = no_outcome_pool_size.value
|
@@ -144,7 +164,255 @@ def get_kelly_bet_full(
|
|
144
164
|
kelly_bet_amount = numerator / denominator
|
145
165
|
|
146
166
|
# Clip the bet size to max_bet to account for rounding errors.
|
147
|
-
return
|
167
|
+
return BinaryKellyBet(
|
148
168
|
direction=kelly_bet_amount > 0,
|
149
169
|
size=CollateralToken(min(max_bet.value, abs(kelly_bet_amount))),
|
150
170
|
)
|
171
|
+
|
172
|
+
|
173
|
+
def get_kelly_bets_categorical_simplified(
|
174
|
+
market_probabilities: list[Probability],
|
175
|
+
estimated_probabilities: list[Probability],
|
176
|
+
confidence: float,
|
177
|
+
max_bet: CollateralToken,
|
178
|
+
fees: MarketFees,
|
179
|
+
allow_multiple_bets: bool,
|
180
|
+
allow_shorting: bool,
|
181
|
+
bet_precision: int = 6,
|
182
|
+
) -> list[CategoricalKellyBet]:
|
183
|
+
"""
|
184
|
+
Calculate Kelly bets for categorical markets using only market probabilities.
|
185
|
+
Returns a list of CategoricalKellyBet objects, one for each outcome.
|
186
|
+
Considers max_bet across all outcomes together.
|
187
|
+
Indicates both buying (long) and shorting (selling) by allowing negative bet sizes.
|
188
|
+
"""
|
189
|
+
for p in chain(market_probabilities, estimated_probabilities, [confidence]):
|
190
|
+
check_is_valid_probability(p)
|
191
|
+
assert len(market_probabilities) == len(
|
192
|
+
estimated_probabilities
|
193
|
+
), "Mismatch in number of outcomes"
|
194
|
+
|
195
|
+
f = 1 - fees.bet_proportion
|
196
|
+
|
197
|
+
total_kelly_fraction = 0.0
|
198
|
+
kelly_fractions = []
|
199
|
+
|
200
|
+
for i in range(len(market_probabilities)):
|
201
|
+
estimated_p = estimated_probabilities[i]
|
202
|
+
market_p = max(market_probabilities[i], 1e-10)
|
203
|
+
|
204
|
+
edge = (estimated_p - market_p) * confidence
|
205
|
+
odds = (1 / market_p) - 1
|
206
|
+
kelly_fraction = edge / odds * f
|
207
|
+
|
208
|
+
if not allow_shorting:
|
209
|
+
kelly_fraction = max(0, kelly_fraction)
|
210
|
+
|
211
|
+
kelly_fractions.append(kelly_fraction)
|
212
|
+
total_kelly_fraction += abs(kelly_fraction)
|
213
|
+
|
214
|
+
best_kelly_fraction_index = max(
|
215
|
+
range(len(kelly_fractions)), key=lambda i: abs(kelly_fractions[i])
|
216
|
+
)
|
217
|
+
|
218
|
+
bets = []
|
219
|
+
for i, kelly_fraction in enumerate(kelly_fractions):
|
220
|
+
if not allow_multiple_bets:
|
221
|
+
bet_size = (
|
222
|
+
kelly_fraction * max_bet.value if i == best_kelly_fraction_index else 0
|
223
|
+
)
|
224
|
+
elif allow_multiple_bets and total_kelly_fraction > 0:
|
225
|
+
bet_size = (kelly_fraction / total_kelly_fraction) * max_bet.value
|
226
|
+
else:
|
227
|
+
bet_size = 0.0
|
228
|
+
# Ensure bet_size is within [-max_bet.value, max_bet.value]
|
229
|
+
bet_size = max(-max_bet.value, min(bet_size, max_bet.value))
|
230
|
+
bets.append(
|
231
|
+
CategoricalKellyBet(
|
232
|
+
index=i, size=CollateralToken(round(bet_size, bet_precision))
|
233
|
+
)
|
234
|
+
)
|
235
|
+
|
236
|
+
return bets
|
237
|
+
|
238
|
+
|
239
|
+
def get_kelly_bets_categorical_full(
|
240
|
+
outcome_pool_sizes: list[OutcomeToken],
|
241
|
+
estimated_probabilities: list[Probability],
|
242
|
+
confidence: float,
|
243
|
+
max_bet: CollateralToken,
|
244
|
+
fees: MarketFees,
|
245
|
+
allow_multiple_bets: bool,
|
246
|
+
allow_shorting: bool,
|
247
|
+
multicategorical: bool,
|
248
|
+
bet_precision: int = 6,
|
249
|
+
) -> list[CategoricalKellyBet]:
|
250
|
+
"""
|
251
|
+
Calculate Kelly bets for categorical markets using joint optimization over all outcomes,
|
252
|
+
splitting the max bet between all possible outcomes to maximize expected log utility.
|
253
|
+
Returns a list of CategoricalKellyBet objects, one for each outcome.
|
254
|
+
Handles both buying (long) and shorting (selling) by allowing negative bet sizes.
|
255
|
+
If the agent's probabilities are very close to the market's, returns all-zero bets.
|
256
|
+
multicategorical means that multiple outcomes could be selected as correct ones.
|
257
|
+
"""
|
258
|
+
assert len(outcome_pool_sizes) == len(
|
259
|
+
estimated_probabilities
|
260
|
+
), "Mismatch in number of outcomes"
|
261
|
+
|
262
|
+
market_probabilities = AgentMarket.compute_fpmm_probabilities(
|
263
|
+
[x.as_outcome_wei for x in outcome_pool_sizes]
|
264
|
+
)
|
265
|
+
|
266
|
+
for p in chain(market_probabilities, estimated_probabilities, [confidence]):
|
267
|
+
check_is_valid_probability(p)
|
268
|
+
|
269
|
+
n = len(outcome_pool_sizes)
|
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 = calculate_buy_outcome_token(
|
287
|
+
CollateralToken(bets[i]), i, outcome_pool_sizes, fees
|
288
|
+
)
|
289
|
+
payout += buy_result.outcome_tokens_received.value
|
290
|
+
else:
|
291
|
+
# If bet is negative, we "short" outcome i by buying all other outcomes
|
292
|
+
for j in range(n):
|
293
|
+
if j == i:
|
294
|
+
continue
|
295
|
+
buy_result = calculate_buy_outcome_token(
|
296
|
+
CollateralToken(abs(bets[i]) / (n - 1)),
|
297
|
+
j,
|
298
|
+
outcome_pool_sizes,
|
299
|
+
fees,
|
300
|
+
)
|
301
|
+
payout += buy_result.outcome_tokens_received.value
|
302
|
+
payouts.append(payout)
|
303
|
+
return payouts
|
304
|
+
|
305
|
+
def adjust_prob(my_prob: float, market_prob: float) -> float:
|
306
|
+
# Based on the confidence, shrinks the predicted probability towards market's current probability.
|
307
|
+
return confidence * my_prob + (1 - confidence) * market_prob
|
308
|
+
|
309
|
+
# Use the simple version to estimate the initial bet vector.
|
310
|
+
x0 = np.array(
|
311
|
+
[
|
312
|
+
x.size.value # Use simplified value as starting point
|
313
|
+
for x in get_kelly_bets_categorical_simplified(
|
314
|
+
market_probabilities=market_probabilities,
|
315
|
+
estimated_probabilities=estimated_probabilities,
|
316
|
+
confidence=confidence,
|
317
|
+
max_bet=max_bet,
|
318
|
+
fees=fees,
|
319
|
+
allow_multiple_bets=allow_multiple_bets,
|
320
|
+
allow_shorting=allow_shorting,
|
321
|
+
bet_precision=bet_precision,
|
322
|
+
)
|
323
|
+
]
|
324
|
+
)
|
325
|
+
|
326
|
+
# Track the best solution found during optimization
|
327
|
+
best_solution_bets = None
|
328
|
+
best_solution_utility = float("-inf")
|
329
|
+
|
330
|
+
def neg_expected_log_utility(bets: list[float]) -> float:
|
331
|
+
"""
|
332
|
+
Negative expected log utility for categorical Kelly betting.
|
333
|
+
This function is minimized to find the optimal bet allocation.
|
334
|
+
"""
|
335
|
+
adj_probs = [
|
336
|
+
adjust_prob(estimated_probabilities[i], market_probabilities[i])
|
337
|
+
for i in range(n)
|
338
|
+
]
|
339
|
+
payouts = compute_payouts(bets)
|
340
|
+
|
341
|
+
profits = [payout - abs(bet) for payout, bet in zip(payouts, bets)]
|
342
|
+
|
343
|
+
# Ensure profits are not too negative to avoid log(negative) or log(0)
|
344
|
+
# Use a small epsilon to prevent numerical instability
|
345
|
+
min_profit = -0.99 # Ensure 1 + profit > 0.01
|
346
|
+
profits = [max(profit, min_profit) for profit in profits]
|
347
|
+
|
348
|
+
# Expected log utility
|
349
|
+
expected_log_utility: float = sum(
|
350
|
+
adj_probs[i] * np.log(1 + profits[i]) for i in range(n)
|
351
|
+
)
|
352
|
+
|
353
|
+
# Track the best solution found so far
|
354
|
+
nonlocal best_solution_bets, best_solution_utility
|
355
|
+
if expected_log_utility > best_solution_utility:
|
356
|
+
best_solution_bets = np.array(bets)
|
357
|
+
best_solution_utility = expected_log_utility
|
358
|
+
|
359
|
+
# Return negative for minimization
|
360
|
+
return -expected_log_utility
|
361
|
+
|
362
|
+
constraints = [
|
363
|
+
# We can not bet more than `max_bet_value`
|
364
|
+
{
|
365
|
+
"type": "ineq",
|
366
|
+
"fun": lambda bets: max_bet_value - np.sum(np.abs(bets)),
|
367
|
+
},
|
368
|
+
# Each bet should not result in guaranteed loss
|
369
|
+
{
|
370
|
+
"type": "ineq",
|
371
|
+
"fun": lambda bets: [
|
372
|
+
payout
|
373
|
+
- (sum(abs(b) for b in bets) if not multicategorical else abs(bets[i]))
|
374
|
+
for i, payout in enumerate(compute_payouts(bets))
|
375
|
+
],
|
376
|
+
},
|
377
|
+
]
|
378
|
+
|
379
|
+
result = minimize(
|
380
|
+
neg_expected_log_utility,
|
381
|
+
x0,
|
382
|
+
method="SLSQP",
|
383
|
+
bounds=[
|
384
|
+
((-max_bet_value if allow_shorting else 0), max_bet_value) for _ in range(n)
|
385
|
+
],
|
386
|
+
constraints=constraints,
|
387
|
+
options={"maxiter": 10_000},
|
388
|
+
)
|
389
|
+
|
390
|
+
# This can sometimes happen, as long as it's occasional, it's should be fine to just use simplified version approximation.
|
391
|
+
if not result.success:
|
392
|
+
logger.warning(
|
393
|
+
f"Joint optimization failed: {result=} {x0=} {estimated_probabilities=} {confidence=} {market_probabilities=}"
|
394
|
+
)
|
395
|
+
|
396
|
+
# Use the best solution found during optimization, not just the final result (result.x).
|
397
|
+
# This is important because SLSQP may end on a worse solution due to numerical issues.
|
398
|
+
bet_vector = check_not_none(best_solution_bets) if result.success else x0
|
399
|
+
|
400
|
+
if not allow_multiple_bets:
|
401
|
+
# If we are not allowing multiple bets, we need to ensure only one bet is non-zero.
|
402
|
+
# We can do this by taking the maximum bet and setting all others to zero.
|
403
|
+
# We do this, instead of enforcing it in with additional constraint,
|
404
|
+
# because such hard constraint is problematic for the solver and results in almost always failing to optimize.
|
405
|
+
max_bet_index = np.argmax(np.abs(bet_vector))
|
406
|
+
max_bet_value = bet_vector[max_bet_index]
|
407
|
+
|
408
|
+
bet_vector = np.zeros_like(bet_vector)
|
409
|
+
bet_vector[max_bet_index] = max_bet_value
|
410
|
+
|
411
|
+
bets = [
|
412
|
+
CategoricalKellyBet(
|
413
|
+
index=i, size=CollateralToken(round(bet_vector[i], bet_precision))
|
414
|
+
)
|
415
|
+
for i in range(n)
|
416
|
+
]
|
417
|
+
|
418
|
+
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
|
@@ -60,6 +60,15 @@ class HexBytes(HexBytesBase, BaseHex):
|
|
60
60
|
value = hex_str[2:] if hex_str.startswith("0x") else hex_str
|
61
61
|
return super().fromhex(value)
|
62
62
|
|
63
|
+
def hex(
|
64
|
+
self,
|
65
|
+
sep: t.Union[str, bytes] | None = None,
|
66
|
+
bytes_per_sep: t.SupportsIndex = 1,
|
67
|
+
) -> str:
|
68
|
+
"""We enforce a 0x prefix."""
|
69
|
+
x = super().hex(sep, bytes_per_sep) # type: ignore[arg-type]
|
70
|
+
return x if x.startswith("0x") else "0x" + x
|
71
|
+
|
63
72
|
@classmethod
|
64
73
|
def __eth_pydantic_validate__(
|
65
74
|
cls, value: t.Any, info: ValidationInfo | None = None
|
@@ -1,15 +1,17 @@
|
|
1
|
+
from datetime import timedelta
|
2
|
+
|
1
3
|
import hishel
|
2
4
|
import httpx
|
3
5
|
|
4
6
|
from prediction_market_agent_tooling.tools.singleton import SingletonMeta
|
5
7
|
|
6
|
-
|
8
|
+
ONE_DAY = timedelta(days=1)
|
7
9
|
|
8
10
|
|
9
11
|
class HttpxCachedClient(metaclass=SingletonMeta):
|
10
|
-
def __init__(self, ttl:
|
12
|
+
def __init__(self, ttl: timedelta = ONE_DAY) -> None:
|
11
13
|
storage = hishel.FileStorage(
|
12
|
-
ttl=ttl,
|
14
|
+
ttl=ttl.total_seconds(),
|
13
15
|
check_ttl_every=60,
|
14
16
|
)
|
15
17
|
controller = hishel.Controller(force_cache=True)
|
@@ -68,6 +68,7 @@ def get_traces_for_agent(
|
|
68
68
|
client: Langfuse,
|
69
69
|
to_timestamp: DatetimeUTC | None = None,
|
70
70
|
tags: str | list[str] | None = None,
|
71
|
+
limit: int | None = None,
|
71
72
|
) -> list[TraceWithDetails]:
|
72
73
|
"""
|
73
74
|
Fetch agent traces using pagination
|
@@ -98,6 +99,9 @@ def get_traces_for_agent(
|
|
98
99
|
if has_output:
|
99
100
|
agent_traces = [t for t in agent_traces if t.output is not None]
|
100
101
|
all_agent_traces.extend(agent_traces)
|
102
|
+
if limit is not None and len(all_agent_traces) >= limit:
|
103
|
+
all_agent_traces = all_agent_traces[:limit]
|
104
|
+
break
|
101
105
|
return all_agent_traces
|
102
106
|
|
103
107
|
|
@@ -155,9 +159,6 @@ def get_trace_for_bet(
|
|
155
159
|
not in WRAPPED_XDAI_CONTRACT_ADDRESS
|
156
160
|
):
|
157
161
|
# TODO: We need to compute bet amount token in USD here, but at the time of bet placement!
|
158
|
-
logger.warning(
|
159
|
-
"This currently works only for WXDAI markets, because we need to compare against USD value."
|
160
|
-
)
|
161
162
|
continue
|
162
163
|
# Cannot use exact comparison due to gas fees
|
163
164
|
if (
|
@@ -8,16 +8,21 @@ class SingletonMeta(type, t.Generic[_T]):
|
|
8
8
|
The Singleton class can be implemented in different ways in Python. Some
|
9
9
|
possible methods include: base class, decorator, metaclass. We will use the
|
10
10
|
metaclass because it is best suited for this purpose.
|
11
|
+
|
12
|
+
This version creates a unique instance for each unique set of __init__ arguments.
|
11
13
|
"""
|
12
14
|
|
13
|
-
_instances: dict[
|
15
|
+
_instances: dict[
|
16
|
+
tuple[t.Any, tuple[t.Any, ...], tuple[tuple[str, t.Any], ...]], _T
|
17
|
+
] = {}
|
14
18
|
|
15
19
|
def __call__(self, *args: t.Any, **kwargs: t.Any) -> _T:
|
16
20
|
"""
|
17
|
-
|
18
|
-
the returned instance.
|
21
|
+
Different __init__ arguments will result in different instances.
|
19
22
|
"""
|
20
|
-
|
23
|
+
# Create a key based on the class, args, and kwargs (sorted for consistency)
|
24
|
+
key = (self, args, tuple(sorted(kwargs.items())))
|
25
|
+
if key not in self._instances:
|
21
26
|
instance = super().__call__(*args, **kwargs)
|
22
|
-
self._instances[
|
23
|
-
return self._instances[
|
27
|
+
self._instances[key] = instance
|
28
|
+
return self._instances[key]
|