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,12 +1,11 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from typing import Sequence
|
|
3
3
|
|
|
4
|
+
import numpy as np
|
|
4
5
|
from scipy.optimize import minimize_scalar
|
|
5
6
|
|
|
6
7
|
from prediction_market_agent_tooling.benchmark.utils import get_most_probable_outcome
|
|
7
|
-
from prediction_market_agent_tooling.deploy.constants import
|
|
8
|
-
INVALID_OUTCOME_LOWERCASE_IDENTIFIER,
|
|
9
|
-
)
|
|
8
|
+
from prediction_market_agent_tooling.deploy.constants import is_invalid_outcome
|
|
10
9
|
from prediction_market_agent_tooling.gtypes import (
|
|
11
10
|
USD,
|
|
12
11
|
CollateralToken,
|
|
@@ -15,7 +14,11 @@ from prediction_market_agent_tooling.gtypes import (
|
|
|
15
14
|
Probability,
|
|
16
15
|
)
|
|
17
16
|
from prediction_market_agent_tooling.loggers import logger
|
|
18
|
-
from prediction_market_agent_tooling.markets.agent_market import
|
|
17
|
+
from prediction_market_agent_tooling.markets.agent_market import (
|
|
18
|
+
AgentMarket,
|
|
19
|
+
MarketFees,
|
|
20
|
+
QuestionType,
|
|
21
|
+
)
|
|
19
22
|
from prediction_market_agent_tooling.markets.data_models import (
|
|
20
23
|
CategoricalProbabilisticAnswer,
|
|
21
24
|
ExistingPosition,
|
|
@@ -23,14 +26,21 @@ from prediction_market_agent_tooling.markets.data_models import (
|
|
|
23
26
|
Trade,
|
|
24
27
|
TradeType,
|
|
25
28
|
)
|
|
29
|
+
from prediction_market_agent_tooling.markets.market_type import MarketType
|
|
26
30
|
from prediction_market_agent_tooling.markets.omen.omen import (
|
|
27
31
|
get_buy_outcome_token_amount,
|
|
28
32
|
)
|
|
29
33
|
from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
|
|
34
|
+
KellyType,
|
|
30
35
|
get_kelly_bet_full,
|
|
31
36
|
get_kelly_bet_simplified,
|
|
37
|
+
get_kelly_bets_categorical_full,
|
|
38
|
+
get_kelly_bets_categorical_simplified,
|
|
39
|
+
)
|
|
40
|
+
from prediction_market_agent_tooling.tools.betting_strategies.utils import (
|
|
41
|
+
BinaryKellyBet,
|
|
42
|
+
CategoricalKellyBet,
|
|
32
43
|
)
|
|
33
|
-
from prediction_market_agent_tooling.tools.betting_strategies.utils import SimpleBet
|
|
34
44
|
from prediction_market_agent_tooling.tools.utils import check_not_none
|
|
35
45
|
|
|
36
46
|
|
|
@@ -39,6 +49,21 @@ class GuaranteedLossError(RuntimeError):
|
|
|
39
49
|
|
|
40
50
|
|
|
41
51
|
class BettingStrategy(ABC):
|
|
52
|
+
supported_question_types: set[QuestionType]
|
|
53
|
+
supported_market_types: set[MarketType]
|
|
54
|
+
|
|
55
|
+
def __init__(self, take_profit: bool = True) -> None:
|
|
56
|
+
self.take_profit = take_profit
|
|
57
|
+
|
|
58
|
+
def is_market_supported(self, market: AgentMarket) -> bool:
|
|
59
|
+
if market.question_type not in self.supported_question_types:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
if MarketType.from_market(market) not in self.supported_market_types:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
return True
|
|
66
|
+
|
|
42
67
|
@abstractmethod
|
|
43
68
|
def calculate_trades(
|
|
44
69
|
self,
|
|
@@ -80,7 +105,7 @@ class BettingStrategy(ABC):
|
|
|
80
105
|
|
|
81
106
|
if outcome_tokens_to_get_in_usd <= trade.amount:
|
|
82
107
|
raise GuaranteedLossError(
|
|
83
|
-
f"Trade {trade=} would result in guaranteed loss by getting only {outcome_tokens_to_get=}. Halting execution."
|
|
108
|
+
f"Trade {trade=} on market {market.url=} would result in guaranteed loss by getting only {outcome_tokens_to_get=}. Halting execution."
|
|
84
109
|
)
|
|
85
110
|
|
|
86
111
|
clean_trades.append(trade)
|
|
@@ -94,6 +119,76 @@ class BettingStrategy(ABC):
|
|
|
94
119
|
)
|
|
95
120
|
return trades
|
|
96
121
|
|
|
122
|
+
@staticmethod
|
|
123
|
+
def cap_to_profitable_bet_amount(
|
|
124
|
+
market: AgentMarket,
|
|
125
|
+
bet_amount: USD,
|
|
126
|
+
outcome: OutcomeStr,
|
|
127
|
+
iters: int = 10,
|
|
128
|
+
) -> USD:
|
|
129
|
+
"""
|
|
130
|
+
Use a binary search (tree-based search) to efficiently find the largest profitable bet amount.
|
|
131
|
+
"""
|
|
132
|
+
# First, try it with the desired amount right away.
|
|
133
|
+
if (
|
|
134
|
+
market.get_in_usd(
|
|
135
|
+
check_not_none(
|
|
136
|
+
market.get_buy_token_amount(bet_amount, outcome)
|
|
137
|
+
).as_token
|
|
138
|
+
)
|
|
139
|
+
> bet_amount
|
|
140
|
+
):
|
|
141
|
+
return bet_amount
|
|
142
|
+
|
|
143
|
+
# If it wasn't profitable, try binary search to find the highest, but profitable, amount.
|
|
144
|
+
lower = USD(0)
|
|
145
|
+
# It doesn't make sense to try to bet more than the liquidity itself, so override it as maximal value if it's lower.
|
|
146
|
+
upper = min(bet_amount, market.get_in_usd(market.get_liquidity()))
|
|
147
|
+
best_profitable = USD(0)
|
|
148
|
+
|
|
149
|
+
for _ in range(iters):
|
|
150
|
+
mid = (lower + upper) / 2
|
|
151
|
+
potential_outcome_value = market.get_in_usd(
|
|
152
|
+
check_not_none(market.get_buy_token_amount(mid, outcome)).as_token
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if potential_outcome_value > mid:
|
|
156
|
+
# Profitable, try higher
|
|
157
|
+
best_profitable = mid
|
|
158
|
+
lower = mid
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
# Not profitable, try lower
|
|
162
|
+
upper = mid
|
|
163
|
+
|
|
164
|
+
# If the search interval is very small, break early
|
|
165
|
+
if float(upper - lower) < 1e-8:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
if np.isclose(best_profitable.value, 0):
|
|
169
|
+
best_profitable = USD(0)
|
|
170
|
+
|
|
171
|
+
return best_profitable
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def cap_to_profitable_position(
|
|
175
|
+
market: AgentMarket,
|
|
176
|
+
existing_position: USD,
|
|
177
|
+
wanted_position: USD,
|
|
178
|
+
outcome_to_bet_on: OutcomeStr,
|
|
179
|
+
) -> USD:
|
|
180
|
+
# If the wanted position is lower, it means the agent is gonna sell and that's profitable always.
|
|
181
|
+
if wanted_position > existing_position:
|
|
182
|
+
difference = wanted_position - existing_position
|
|
183
|
+
# Cap the difference we would like to buy to a profitable one.
|
|
184
|
+
capped_difference = BettingStrategy.cap_to_profitable_bet_amount(
|
|
185
|
+
market, difference, outcome_to_bet_on
|
|
186
|
+
)
|
|
187
|
+
# Lowered the actual wanted position such that it remains profitable.
|
|
188
|
+
wanted_position = existing_position + capped_difference
|
|
189
|
+
|
|
190
|
+
return wanted_position
|
|
191
|
+
|
|
97
192
|
def _build_rebalance_trades_from_positions(
|
|
98
193
|
self,
|
|
99
194
|
existing_position: ExistingPosition | None,
|
|
@@ -138,10 +233,22 @@ class BettingStrategy(ABC):
|
|
|
138
233
|
)
|
|
139
234
|
|
|
140
235
|
diff_amount = target_amount - existing_amount
|
|
236
|
+
|
|
141
237
|
if diff_amount == 0:
|
|
142
238
|
continue
|
|
143
239
|
|
|
144
240
|
trade_type = TradeType.SELL if diff_amount < 0 else TradeType.BUY
|
|
241
|
+
|
|
242
|
+
# We work with positions, so imagine following scenario: Agent invested $10 when probs were 50:50,
|
|
243
|
+
# now the probs are 99:1 and his initial $10 is worth $100.
|
|
244
|
+
# If `take_profit` is set to False, agent won't sell the $90 to get back to the $10 position.
|
|
245
|
+
if (
|
|
246
|
+
not self.take_profit
|
|
247
|
+
and target_amount > 0
|
|
248
|
+
and trade_type == TradeType.SELL
|
|
249
|
+
):
|
|
250
|
+
continue
|
|
251
|
+
|
|
145
252
|
trade = Trade(
|
|
146
253
|
amount=abs(diff_amount),
|
|
147
254
|
outcome=outcome,
|
|
@@ -159,13 +266,21 @@ class BettingStrategy(ABC):
|
|
|
159
266
|
return trades
|
|
160
267
|
|
|
161
268
|
|
|
162
|
-
class
|
|
163
|
-
|
|
164
|
-
|
|
269
|
+
class CategoricalMaxAccuracyBettingStrategy(BettingStrategy):
|
|
270
|
+
supported_question_types = {
|
|
271
|
+
QuestionType.BINARY,
|
|
272
|
+
QuestionType.CATEGORICAL,
|
|
273
|
+
QuestionType.SCALAR,
|
|
274
|
+
}
|
|
275
|
+
supported_market_types = {x for x in MarketType if x.is_trading_market}
|
|
276
|
+
|
|
277
|
+
def __init__(self, max_position_amount: USD, take_profit: bool = True):
|
|
278
|
+
super().__init__(take_profit=take_profit)
|
|
279
|
+
self.max_position_amount = max_position_amount
|
|
165
280
|
|
|
166
281
|
@property
|
|
167
282
|
def maximum_possible_bet_amount(self) -> USD:
|
|
168
|
-
return self.
|
|
283
|
+
return self.max_position_amount
|
|
169
284
|
|
|
170
285
|
@staticmethod
|
|
171
286
|
def calculate_direction(
|
|
@@ -185,7 +300,7 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
|
|
|
185
300
|
) -> OutcomeStr:
|
|
186
301
|
# We get the first direction which is != direction.
|
|
187
302
|
other_direction = [i for i in outcomes if i.lower() != direction.lower()][0]
|
|
188
|
-
if
|
|
303
|
+
if is_invalid_outcome(other_direction):
|
|
189
304
|
raise ValueError("Invalid outcome found as opposite direction. Exitting.")
|
|
190
305
|
return other_direction
|
|
191
306
|
|
|
@@ -196,11 +311,23 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
|
|
|
196
311
|
market: AgentMarket,
|
|
197
312
|
) -> list[Trade]:
|
|
198
313
|
"""We place bet on only one outcome."""
|
|
199
|
-
|
|
200
314
|
outcome_to_bet_on = self.calculate_direction(market, answer)
|
|
201
315
|
|
|
316
|
+
# Will be lowered if the amount that we would need to buy would be unprofitable.
|
|
317
|
+
actual_wanted_position = BettingStrategy.cap_to_profitable_position(
|
|
318
|
+
market,
|
|
319
|
+
(
|
|
320
|
+
existing_position.amounts_current.get(outcome_to_bet_on, USD(0))
|
|
321
|
+
if existing_position
|
|
322
|
+
else USD(0)
|
|
323
|
+
),
|
|
324
|
+
self.max_position_amount,
|
|
325
|
+
outcome_to_bet_on,
|
|
326
|
+
)
|
|
327
|
+
|
|
202
328
|
target_position = Position(
|
|
203
|
-
market_id=market.id,
|
|
329
|
+
market_id=market.id,
|
|
330
|
+
amounts_current={outcome_to_bet_on: actual_wanted_position},
|
|
204
331
|
)
|
|
205
332
|
trades = self._build_rebalance_trades_from_positions(
|
|
206
333
|
existing_position=existing_position,
|
|
@@ -209,8 +336,11 @@ class MultiCategoricalMaxAccuracyBettingStrategy(BettingStrategy):
|
|
|
209
336
|
)
|
|
210
337
|
return trades
|
|
211
338
|
|
|
339
|
+
def __repr__(self) -> str:
|
|
340
|
+
return f"CategoricalMaxAccuracyBettingStrategy(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
|
|
341
|
+
|
|
212
342
|
|
|
213
|
-
class MaxExpectedValueBettingStrategy(
|
|
343
|
+
class MaxExpectedValueBettingStrategy(CategoricalMaxAccuracyBettingStrategy):
|
|
214
344
|
@staticmethod
|
|
215
345
|
def calculate_direction(
|
|
216
346
|
market: AgentMarket, answer: CategoricalProbabilisticAnswer
|
|
@@ -251,43 +381,54 @@ class MaxExpectedValueBettingStrategy(MultiCategoricalMaxAccuracyBettingStrategy
|
|
|
251
381
|
|
|
252
382
|
return best_outcome
|
|
253
383
|
|
|
384
|
+
def __repr__(self) -> str:
|
|
385
|
+
return f"MaxExpectedValueBettingStrategy(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
|
|
386
|
+
|
|
254
387
|
|
|
255
|
-
class
|
|
256
|
-
|
|
257
|
-
|
|
388
|
+
class _BinaryKellyBettingStrategy(BettingStrategy):
|
|
389
|
+
supported_question_types = {QuestionType.BINARY}
|
|
390
|
+
|
|
391
|
+
def __init__(
|
|
392
|
+
self,
|
|
393
|
+
kelly_type: KellyType,
|
|
394
|
+
max_position_amount: USD,
|
|
395
|
+
max_price_impact: float | None = None,
|
|
396
|
+
take_profit: bool = True,
|
|
397
|
+
):
|
|
398
|
+
super().__init__(take_profit=take_profit)
|
|
399
|
+
self.kelly_type = kelly_type
|
|
400
|
+
self.max_position_amount = max_position_amount
|
|
258
401
|
self.max_price_impact = max_price_impact
|
|
259
402
|
|
|
260
403
|
@property
|
|
261
404
|
def maximum_possible_bet_amount(self) -> USD:
|
|
262
|
-
return self.
|
|
405
|
+
return self.max_position_amount
|
|
263
406
|
|
|
264
|
-
@staticmethod
|
|
265
407
|
def get_kelly_bet(
|
|
408
|
+
self,
|
|
266
409
|
market: AgentMarket,
|
|
267
|
-
max_bet_amount: USD,
|
|
268
410
|
direction: OutcomeStr,
|
|
269
411
|
other_direction: OutcomeStr,
|
|
270
412
|
answer: CategoricalProbabilisticAnswer,
|
|
271
413
|
override_p_yes: float | None = None,
|
|
272
|
-
) ->
|
|
414
|
+
) -> BinaryKellyBet:
|
|
415
|
+
if not market.is_binary:
|
|
416
|
+
raise ValueError("This strategy is usable only with binary markets.")
|
|
417
|
+
|
|
273
418
|
estimated_p_yes = (
|
|
274
419
|
answer.probability_for_market_outcome(direction)
|
|
275
420
|
if not override_p_yes
|
|
276
421
|
else override_p_yes
|
|
277
422
|
)
|
|
278
423
|
|
|
279
|
-
if
|
|
280
|
-
# use Kelly simple, since Kelly full only supports 2 outcomes
|
|
281
|
-
|
|
424
|
+
if self.kelly_type == KellyType.SIMPLE:
|
|
282
425
|
kelly_bet = get_kelly_bet_simplified(
|
|
283
|
-
max_bet=market.get_usd_in_token(
|
|
426
|
+
max_bet=market.get_usd_in_token(self.max_position_amount),
|
|
284
427
|
market_p_yes=market.probability_for_market_outcome(direction),
|
|
285
428
|
estimated_p_yes=estimated_p_yes,
|
|
286
429
|
confidence=answer.confidence,
|
|
287
430
|
)
|
|
288
431
|
else:
|
|
289
|
-
# We consider only binary markets, since the Kelly strategy is not yet implemented
|
|
290
|
-
# for markets with more than 2 outcomes (https://github.com/gnosis/prediction-market-agent-tooling/issues/671).
|
|
291
432
|
direction_to_bet_pool_size = market.get_outcome_token_pool_by_outcome(
|
|
292
433
|
direction
|
|
293
434
|
)
|
|
@@ -298,7 +439,7 @@ class KellyBettingStrategy(BettingStrategy):
|
|
|
298
439
|
yes_outcome_pool_size=direction_to_bet_pool_size,
|
|
299
440
|
no_outcome_pool_size=other_direction_pool_size,
|
|
300
441
|
estimated_p_yes=estimated_p_yes,
|
|
301
|
-
max_bet=market.get_usd_in_token(
|
|
442
|
+
max_bet=market.get_usd_in_token(self.max_position_amount),
|
|
302
443
|
confidence=answer.confidence,
|
|
303
444
|
fees=market.fees,
|
|
304
445
|
)
|
|
@@ -311,17 +452,16 @@ class KellyBettingStrategy(BettingStrategy):
|
|
|
311
452
|
market: AgentMarket,
|
|
312
453
|
) -> list[Trade]:
|
|
313
454
|
# We consider the p_yes as the direction with highest probability.
|
|
314
|
-
direction =
|
|
455
|
+
direction = CategoricalMaxAccuracyBettingStrategy.calculate_direction(
|
|
315
456
|
market, answer
|
|
316
457
|
)
|
|
317
458
|
# We get the first direction which is != direction.
|
|
318
459
|
other_direction = [i for i in market.outcomes if i != direction][0]
|
|
319
|
-
if
|
|
460
|
+
if is_invalid_outcome(other_direction):
|
|
320
461
|
raise ValueError("Invalid outcome found as opposite direction. Exitting.")
|
|
321
462
|
|
|
322
463
|
kelly_bet = self.get_kelly_bet(
|
|
323
464
|
market=market,
|
|
324
|
-
max_bet_amount=self.max_bet_amount,
|
|
325
465
|
direction=direction,
|
|
326
466
|
other_direction=other_direction,
|
|
327
467
|
answer=answer,
|
|
@@ -331,15 +471,24 @@ class KellyBettingStrategy(BettingStrategy):
|
|
|
331
471
|
if self.max_price_impact:
|
|
332
472
|
# Adjust amount
|
|
333
473
|
max_price_impact_bet_amount = self.calculate_bet_amount_for_price_impact(
|
|
334
|
-
market,
|
|
474
|
+
market,
|
|
475
|
+
direction=direction,
|
|
476
|
+
max_price_impact=self.max_price_impact,
|
|
335
477
|
)
|
|
336
478
|
|
|
337
479
|
# We just don't want Kelly size to extrapolate price_impact - hence we take the min.
|
|
338
480
|
kelly_bet_size = min(kelly_bet.size, max_price_impact_bet_amount)
|
|
339
481
|
|
|
340
482
|
bet_outcome = direction if kelly_bet.direction else other_direction
|
|
483
|
+
|
|
341
484
|
amounts = {
|
|
342
|
-
bet_outcome:
|
|
485
|
+
bet_outcome: (
|
|
486
|
+
BettingStrategy.cap_to_profitable_bet_amount(
|
|
487
|
+
market, market.get_token_in_usd(kelly_bet_size), bet_outcome
|
|
488
|
+
)
|
|
489
|
+
if kelly_bet_size > 0
|
|
490
|
+
else USD(0)
|
|
491
|
+
),
|
|
343
492
|
}
|
|
344
493
|
target_position = Position(market_id=market.id, amounts_current=amounts)
|
|
345
494
|
trades = self._build_rebalance_trades_from_positions(
|
|
@@ -347,8 +496,8 @@ class KellyBettingStrategy(BettingStrategy):
|
|
|
347
496
|
)
|
|
348
497
|
return trades
|
|
349
498
|
|
|
499
|
+
@staticmethod
|
|
350
500
|
def calculate_price_impact_for_bet_amount(
|
|
351
|
-
self,
|
|
352
501
|
outcome_idx: int,
|
|
353
502
|
bet_amount: CollateralToken,
|
|
354
503
|
pool_balances: list[OutcomeWei],
|
|
@@ -366,29 +515,31 @@ class KellyBettingStrategy(BettingStrategy):
|
|
|
366
515
|
price_impact = (actual_price - expected_price) / expected_price
|
|
367
516
|
return price_impact
|
|
368
517
|
|
|
518
|
+
@staticmethod
|
|
369
519
|
def calculate_bet_amount_for_price_impact(
|
|
370
|
-
|
|
520
|
+
market: AgentMarket,
|
|
521
|
+
direction: OutcomeStr,
|
|
522
|
+
max_price_impact: float,
|
|
371
523
|
) -> CollateralToken:
|
|
372
524
|
def calculate_price_impact_deviation_from_target_price_impact(
|
|
373
|
-
|
|
525
|
+
bet_amount_collateral: float, # Needs to be float because it's used in minimize_scalar internally.
|
|
374
526
|
) -> float:
|
|
375
527
|
outcome_idx = market.get_outcome_index(direction)
|
|
376
|
-
price_impact =
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
528
|
+
price_impact = (
|
|
529
|
+
_BinaryKellyBettingStrategy.calculate_price_impact_for_bet_amount(
|
|
530
|
+
outcome_idx=outcome_idx,
|
|
531
|
+
bet_amount=CollateralToken(bet_amount_collateral),
|
|
532
|
+
pool_balances=pool_balances,
|
|
533
|
+
fees=market.fees,
|
|
534
|
+
)
|
|
381
535
|
)
|
|
382
536
|
# We return abs for the algorithm to converge to 0 instead of the min (and possibly negative) value.
|
|
383
|
-
|
|
384
|
-
max_price_impact = check_not_none(self.max_price_impact)
|
|
385
537
|
return abs(price_impact - max_price_impact)
|
|
386
538
|
|
|
387
539
|
if not market.outcome_token_pool:
|
|
388
|
-
|
|
540
|
+
raise ValueError(
|
|
389
541
|
"Market outcome_token_pool is None, cannot calculate bet amount"
|
|
390
542
|
)
|
|
391
|
-
return kelly_bet.size
|
|
392
543
|
|
|
393
544
|
pool_balances = [i.as_outcome_wei for i in market.outcome_token_pool.values()]
|
|
394
545
|
# stay float for compatibility with `minimize_scalar`
|
|
@@ -399,30 +550,64 @@ class KellyBettingStrategy(BettingStrategy):
|
|
|
399
550
|
calculate_price_impact_deviation_from_target_price_impact,
|
|
400
551
|
bounds=(0, 1000 * total_pool_balance),
|
|
401
552
|
method="bounded",
|
|
402
|
-
tol=1e-
|
|
553
|
+
tol=1e-13,
|
|
403
554
|
options={"maxiter": 10000},
|
|
404
555
|
)
|
|
405
556
|
return CollateralToken(optimized_bet_amount.x)
|
|
406
557
|
|
|
407
558
|
def __repr__(self) -> str:
|
|
408
|
-
return f"{self.__class__.__name__}(
|
|
559
|
+
return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, max_price_impact={self.max_price_impact}, take_profit={self.take_profit})"
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class SimpleBinaryKellyBettingStrategy(_BinaryKellyBettingStrategy):
|
|
563
|
+
supported_market_types = {x for x in MarketType if x.is_trading_market}
|
|
564
|
+
|
|
565
|
+
def __init__(
|
|
566
|
+
self,
|
|
567
|
+
max_position_amount: USD,
|
|
568
|
+
take_profit: bool = True,
|
|
569
|
+
):
|
|
570
|
+
super().__init__(
|
|
571
|
+
kelly_type=KellyType.SIMPLE,
|
|
572
|
+
max_position_amount=max_position_amount,
|
|
573
|
+
max_price_impact=None,
|
|
574
|
+
take_profit=take_profit,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class FullBinaryKellyBettingStrategy(_BinaryKellyBettingStrategy):
|
|
579
|
+
# Supports only OMEN because it uses closed-form formula derived from a binary FPMM.
|
|
580
|
+
supported_market_types = {MarketType.OMEN}
|
|
581
|
+
|
|
582
|
+
def __init__(
|
|
583
|
+
self,
|
|
584
|
+
max_position_amount: USD,
|
|
585
|
+
max_price_impact: float | None = None,
|
|
586
|
+
take_profit: bool = True,
|
|
587
|
+
):
|
|
588
|
+
super().__init__(
|
|
589
|
+
kelly_type=KellyType.FULL,
|
|
590
|
+
max_position_amount=max_position_amount,
|
|
591
|
+
max_price_impact=max_price_impact,
|
|
592
|
+
take_profit=take_profit,
|
|
593
|
+
)
|
|
409
594
|
|
|
410
595
|
|
|
411
596
|
class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
|
|
412
|
-
|
|
413
|
-
|
|
597
|
+
supported_question_types = {QuestionType.BINARY}
|
|
598
|
+
supported_market_types = {MarketType.OMEN}
|
|
599
|
+
|
|
600
|
+
def __init__(
|
|
601
|
+
self,
|
|
602
|
+
max_position_amount: USD,
|
|
603
|
+
take_profit: bool = True,
|
|
604
|
+
):
|
|
605
|
+
super().__init__(take_profit)
|
|
606
|
+
self.max_position_amount = max_position_amount
|
|
414
607
|
|
|
415
608
|
@property
|
|
416
609
|
def maximum_possible_bet_amount(self) -> USD:
|
|
417
|
-
return self.
|
|
418
|
-
|
|
419
|
-
def adjust_bet_amount(
|
|
420
|
-
self, existing_position: ExistingPosition | None, market: AgentMarket
|
|
421
|
-
) -> USD:
|
|
422
|
-
existing_position_total_amount = (
|
|
423
|
-
existing_position.total_amount_current if existing_position else USD(0)
|
|
424
|
-
)
|
|
425
|
-
return self.max_bet_amount + existing_position_total_amount
|
|
610
|
+
return self.max_position_amount
|
|
426
611
|
|
|
427
612
|
def calculate_trades(
|
|
428
613
|
self,
|
|
@@ -430,26 +615,23 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
|
|
|
430
615
|
answer: CategoricalProbabilisticAnswer,
|
|
431
616
|
market: AgentMarket,
|
|
432
617
|
) -> list[Trade]:
|
|
433
|
-
adjusted_bet_amount_usd = self.adjust_bet_amount(existing_position, market)
|
|
434
|
-
|
|
435
618
|
outcome = get_most_probable_outcome(answer.probabilities)
|
|
436
619
|
|
|
437
|
-
direction =
|
|
620
|
+
direction = CategoricalMaxAccuracyBettingStrategy.calculate_direction(
|
|
438
621
|
market, answer
|
|
439
622
|
)
|
|
440
623
|
# We get the first direction which is != direction.
|
|
441
|
-
other_direction = (
|
|
442
|
-
|
|
443
|
-
outcomes=market.outcomes, direction=direction
|
|
444
|
-
)
|
|
624
|
+
other_direction = CategoricalMaxAccuracyBettingStrategy.get_other_direction(
|
|
625
|
+
outcomes=market.outcomes, direction=direction
|
|
445
626
|
)
|
|
446
627
|
|
|
447
628
|
# We ignore the direction nudge given by Kelly, hence we assume we have a perfect prediction.
|
|
448
629
|
estimated_p_yes = 1.0
|
|
449
630
|
|
|
450
|
-
kelly_bet =
|
|
631
|
+
kelly_bet = FullBinaryKellyBettingStrategy(
|
|
632
|
+
max_position_amount=self.max_position_amount
|
|
633
|
+
).get_kelly_bet(
|
|
451
634
|
market=market,
|
|
452
|
-
max_bet_amount=adjusted_bet_amount_usd,
|
|
453
635
|
direction=direction,
|
|
454
636
|
other_direction=other_direction,
|
|
455
637
|
answer=answer,
|
|
@@ -471,4 +653,166 @@ class MaxAccuracyWithKellyScaledBetsStrategy(BettingStrategy):
|
|
|
471
653
|
return trades
|
|
472
654
|
|
|
473
655
|
def __repr__(self) -> str:
|
|
474
|
-
return f"{self.__class__.__name__}(
|
|
656
|
+
return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, take_profit={self.take_profit})"
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class _CategoricalKellyBettingStrategy(BettingStrategy):
|
|
660
|
+
supported_question_types = {QuestionType.BINARY, QuestionType.CATEGORICAL}
|
|
661
|
+
supported_market_types = {x for x in MarketType if x.is_trading_market}
|
|
662
|
+
|
|
663
|
+
def __init__(
|
|
664
|
+
self,
|
|
665
|
+
kelly_type: KellyType,
|
|
666
|
+
max_position_amount: USD,
|
|
667
|
+
max_price_impact: float | None,
|
|
668
|
+
allow_multiple_bets: bool,
|
|
669
|
+
allow_shorting: bool,
|
|
670
|
+
multicategorical: bool,
|
|
671
|
+
take_profit: bool = True,
|
|
672
|
+
):
|
|
673
|
+
super().__init__(take_profit=take_profit)
|
|
674
|
+
self.kelly_type = kelly_type
|
|
675
|
+
self.max_position_amount = max_position_amount
|
|
676
|
+
self.max_price_impact = max_price_impact
|
|
677
|
+
self.allow_multiple_bets = allow_multiple_bets
|
|
678
|
+
self.allow_shorting = allow_shorting
|
|
679
|
+
self.multicategorical = multicategorical
|
|
680
|
+
|
|
681
|
+
@property
|
|
682
|
+
def maximum_possible_bet_amount(self) -> USD:
|
|
683
|
+
return self.max_position_amount
|
|
684
|
+
|
|
685
|
+
def get_kelly_bets(
|
|
686
|
+
self,
|
|
687
|
+
market: AgentMarket,
|
|
688
|
+
max_bet_amount: USD,
|
|
689
|
+
answer: CategoricalProbabilisticAnswer,
|
|
690
|
+
) -> list[CategoricalKellyBet]:
|
|
691
|
+
max_bet = market.get_usd_in_token(max_bet_amount)
|
|
692
|
+
|
|
693
|
+
if self.kelly_type == KellyType.SIMPLE:
|
|
694
|
+
kelly_bets = get_kelly_bets_categorical_simplified(
|
|
695
|
+
market_probabilities=[market.probabilities[o] for o in market.outcomes],
|
|
696
|
+
estimated_probabilities=[
|
|
697
|
+
answer.probability_for_market_outcome(o) for o in market.outcomes
|
|
698
|
+
],
|
|
699
|
+
confidence=answer.confidence,
|
|
700
|
+
max_bet=max_bet,
|
|
701
|
+
fees=market.fees,
|
|
702
|
+
allow_multiple_bets=self.allow_multiple_bets,
|
|
703
|
+
allow_shorting=self.allow_shorting,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
else:
|
|
707
|
+
kelly_bets = get_kelly_bets_categorical_full(
|
|
708
|
+
market_probabilities=[
|
|
709
|
+
market.probability_for_market_outcome(o) for o in market.outcomes
|
|
710
|
+
],
|
|
711
|
+
estimated_probabilities=[
|
|
712
|
+
answer.probability_for_market_outcome(o) for o in market.outcomes
|
|
713
|
+
],
|
|
714
|
+
confidence=answer.confidence,
|
|
715
|
+
max_bet=max_bet,
|
|
716
|
+
fees=market.fees,
|
|
717
|
+
allow_multiple_bets=self.allow_multiple_bets,
|
|
718
|
+
allow_shorting=self.allow_shorting,
|
|
719
|
+
multicategorical=self.multicategorical,
|
|
720
|
+
get_buy_token_amount=lambda bet_amount, outcome_index: check_not_none(
|
|
721
|
+
market.get_buy_token_amount(
|
|
722
|
+
bet_amount, market.get_outcome_str(outcome_index)
|
|
723
|
+
)
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
return kelly_bets
|
|
728
|
+
|
|
729
|
+
def calculate_trades(
|
|
730
|
+
self,
|
|
731
|
+
existing_position: ExistingPosition | None,
|
|
732
|
+
answer: CategoricalProbabilisticAnswer,
|
|
733
|
+
market: AgentMarket,
|
|
734
|
+
) -> list[Trade]:
|
|
735
|
+
kelly_bets = self.get_kelly_bets(
|
|
736
|
+
market=market,
|
|
737
|
+
max_bet_amount=self.max_position_amount,
|
|
738
|
+
answer=answer,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# TODO: Allow shorting in BettingStrategy._build_rebalance_trades_from_positions.
|
|
742
|
+
# In binary implementation, we simply flip the direction in case of negative bet, for categorical outcome, we need to implement shorting.
|
|
743
|
+
kelly_bets = [bet for bet in kelly_bets if bet.size > 0]
|
|
744
|
+
if not kelly_bets:
|
|
745
|
+
return []
|
|
746
|
+
|
|
747
|
+
# TODO: Allow betting on multiple outcomes.
|
|
748
|
+
# Categorical kelly could suggest to bet on multiple outcomes, but we only consider the first one for now (limitation of BettingStrategy `trades` creation).
|
|
749
|
+
# Also, this could maybe work for multi-categorical markets as well, but it wasn't benchmarked for it.
|
|
750
|
+
best_kelly_bet = max(kelly_bets, key=lambda x: abs(x.size))
|
|
751
|
+
|
|
752
|
+
if self.max_price_impact:
|
|
753
|
+
# Adjust amount
|
|
754
|
+
max_price_impact_bet_amount = (
|
|
755
|
+
_BinaryKellyBettingStrategy.calculate_bet_amount_for_price_impact(
|
|
756
|
+
market,
|
|
757
|
+
direction=market.get_outcome_str(best_kelly_bet.index),
|
|
758
|
+
max_price_impact=self.max_price_impact,
|
|
759
|
+
)
|
|
760
|
+
)
|
|
761
|
+
# We just don't want Kelly size to extrapolate price_impact - hence we take the min.
|
|
762
|
+
best_kelly_bet.size = min(best_kelly_bet.size, max_price_impact_bet_amount)
|
|
763
|
+
|
|
764
|
+
amounts = {
|
|
765
|
+
market.outcomes[best_kelly_bet.index]: market.get_token_in_usd(
|
|
766
|
+
best_kelly_bet.size
|
|
767
|
+
),
|
|
768
|
+
}
|
|
769
|
+
target_position = Position(market_id=market.id, amounts_current=amounts)
|
|
770
|
+
trades = self._build_rebalance_trades_from_positions(
|
|
771
|
+
existing_position, target_position, market=market
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
return trades
|
|
775
|
+
|
|
776
|
+
def __repr__(self) -> str:
|
|
777
|
+
return f"{self.__class__.__name__}(max_position_amount={self.max_position_amount}, max_price_impact={self.max_price_impact}, allow_multiple_bets={self.allow_multiple_bets}, allow_shorting={self.allow_shorting}, take_profit={self.take_profit})"
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
class SimpleCategoricalKellyBettingStrategy(_CategoricalKellyBettingStrategy):
|
|
781
|
+
def __init__(
|
|
782
|
+
self,
|
|
783
|
+
max_position_amount: USD,
|
|
784
|
+
allow_multiple_bets: bool,
|
|
785
|
+
allow_shorting: bool,
|
|
786
|
+
multicategorical: bool,
|
|
787
|
+
take_profit: bool = True,
|
|
788
|
+
):
|
|
789
|
+
super().__init__(
|
|
790
|
+
kelly_type=KellyType.SIMPLE,
|
|
791
|
+
max_position_amount=max_position_amount,
|
|
792
|
+
max_price_impact=None,
|
|
793
|
+
allow_multiple_bets=allow_multiple_bets,
|
|
794
|
+
allow_shorting=allow_shorting,
|
|
795
|
+
multicategorical=multicategorical,
|
|
796
|
+
take_profit=take_profit,
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
class FullCategoricalKellyBettingStrategy(_CategoricalKellyBettingStrategy):
|
|
801
|
+
def __init__(
|
|
802
|
+
self,
|
|
803
|
+
max_position_amount: USD,
|
|
804
|
+
max_price_impact: float | None,
|
|
805
|
+
allow_multiple_bets: bool,
|
|
806
|
+
allow_shorting: bool,
|
|
807
|
+
multicategorical: bool,
|
|
808
|
+
take_profit: bool = True,
|
|
809
|
+
):
|
|
810
|
+
super().__init__(
|
|
811
|
+
kelly_type=KellyType.FULL,
|
|
812
|
+
max_position_amount=max_position_amount,
|
|
813
|
+
max_price_impact=max_price_impact,
|
|
814
|
+
allow_multiple_bets=allow_multiple_bets,
|
|
815
|
+
allow_shorting=allow_shorting,
|
|
816
|
+
multicategorical=multicategorical,
|
|
817
|
+
take_profit=take_profit,
|
|
818
|
+
)
|