prediction-market-agent-tooling 0.64.12.dev659__py3-none-any.whl → 0.65.0__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/benchmark/agents.py +19 -16
- prediction_market_agent_tooling/benchmark/benchmark.py +94 -84
- prediction_market_agent_tooling/benchmark/utils.py +8 -9
- prediction_market_agent_tooling/chains.py +4 -0
- prediction_market_agent_tooling/config.py +4 -3
- prediction_market_agent_tooling/deploy/agent.py +85 -125
- prediction_market_agent_tooling/deploy/agent_example.py +20 -10
- prediction_market_agent_tooling/deploy/betting_strategy.py +222 -96
- prediction_market_agent_tooling/deploy/constants.py +4 -0
- prediction_market_agent_tooling/jobs/jobs_models.py +15 -4
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +3 -3
- prediction_market_agent_tooling/markets/agent_market.py +145 -50
- prediction_market_agent_tooling/markets/blockchain_utils.py +10 -1
- prediction_market_agent_tooling/markets/data_models.py +83 -17
- prediction_market_agent_tooling/markets/manifold/api.py +18 -7
- prediction_market_agent_tooling/markets/manifold/data_models.py +23 -16
- prediction_market_agent_tooling/markets/manifold/manifold.py +18 -18
- prediction_market_agent_tooling/markets/manifold/utils.py +7 -12
- prediction_market_agent_tooling/markets/markets.py +2 -1
- prediction_market_agent_tooling/markets/metaculus/metaculus.py +29 -4
- prediction_market_agent_tooling/markets/omen/data_models.py +17 -32
- prediction_market_agent_tooling/markets/omen/omen.py +65 -108
- prediction_market_agent_tooling/markets/omen/omen_contracts.py +2 -5
- prediction_market_agent_tooling/markets/omen/omen_resolving.py +13 -13
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +18 -12
- prediction_market_agent_tooling/markets/polymarket/data_models.py +7 -3
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +7 -3
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +5 -4
- prediction_market_agent_tooling/markets/seer/data_models.py +0 -83
- prediction_market_agent_tooling/markets/seer/price_manager.py +44 -30
- prediction_market_agent_tooling/markets/seer/seer.py +105 -105
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +34 -41
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +1 -1
- prediction_market_agent_tooling/tools/cow/cow_order.py +10 -3
- prediction_market_agent_tooling/tools/is_predictable.py +2 -3
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +4 -4
- prediction_market_agent_tooling/tools/omen/sell_positions.py +3 -2
- prediction_market_agent_tooling/tools/utils.py +26 -13
- {prediction_market_agent_tooling-0.64.12.dev659.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/METADATA +2 -2
- {prediction_market_agent_tooling-0.64.12.dev659.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/RECORD +43 -52
- prediction_market_agent_tooling/monitor/financial_metrics/financial_metrics.py +0 -68
- prediction_market_agent_tooling/monitor/markets/manifold.py +0 -90
- prediction_market_agent_tooling/monitor/markets/metaculus.py +0 -43
- prediction_market_agent_tooling/monitor/markets/omen.py +0 -88
- prediction_market_agent_tooling/monitor/markets/polymarket.py +0 -49
- prediction_market_agent_tooling/monitor/monitor.py +0 -406
- prediction_market_agent_tooling/monitor/monitor_app.py +0 -149
- prediction_market_agent_tooling/monitor/monitor_settings.py +0 -27
- prediction_market_agent_tooling/tools/betting_strategies/market_moving.py +0 -146
- prediction_market_agent_tooling/tools/betting_strategies/minimum_bet_to_win.py +0 -12
- {prediction_market_agent_tooling-0.64.12.dev659.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/LICENSE +0 -0
- {prediction_market_agent_tooling-0.64.12.dev659.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/WHEEL +0 -0
- {prediction_market_agent_tooling-0.64.12.dev659.dist-info → prediction_market_agent_tooling-0.65.0.dist-info}/entry_points.txt +0 -0
@@ -1,25 +1,33 @@
|
|
1
1
|
import typing as t
|
2
2
|
from enum import Enum
|
3
|
+
from math import prod
|
3
4
|
|
4
5
|
from eth_typing import ChecksumAddress
|
5
6
|
from pydantic import BaseModel, field_validator, model_validator
|
6
7
|
from pydantic_core.core_schema import FieldValidationInfo
|
7
8
|
from web3 import Web3
|
8
9
|
|
10
|
+
from prediction_market_agent_tooling.benchmark.utils import get_most_probable_outcome
|
9
11
|
from prediction_market_agent_tooling.config import APIKeys
|
12
|
+
from prediction_market_agent_tooling.deploy.constants import (
|
13
|
+
INVALID_OUTCOME_LOWERCASE_IDENTIFIER,
|
14
|
+
NO_OUTCOME_LOWERCASE_IDENTIFIER,
|
15
|
+
YES_OUTCOME_LOWERCASE_IDENTIFIER,
|
16
|
+
)
|
10
17
|
from prediction_market_agent_tooling.gtypes import (
|
11
|
-
CollateralToken,
|
12
18
|
OutcomeStr,
|
13
19
|
OutcomeToken,
|
20
|
+
OutcomeWei,
|
14
21
|
Probability,
|
15
22
|
)
|
23
|
+
from prediction_market_agent_tooling.loggers import logger
|
16
24
|
from prediction_market_agent_tooling.markets.data_models import (
|
17
25
|
USD,
|
18
26
|
Bet,
|
27
|
+
CategoricalProbabilisticAnswer,
|
19
28
|
CollateralToken,
|
20
29
|
ExistingPosition,
|
21
30
|
PlacedTrade,
|
22
|
-
ProbabilisticAnswer,
|
23
31
|
Resolution,
|
24
32
|
ResolvedBet,
|
25
33
|
)
|
@@ -27,13 +35,12 @@ from prediction_market_agent_tooling.markets.market_fees import MarketFees
|
|
27
35
|
from prediction_market_agent_tooling.tools.utils import (
|
28
36
|
DatetimeUTC,
|
29
37
|
check_not_none,
|
30
|
-
should_not_happen,
|
31
38
|
utcnow,
|
32
39
|
)
|
33
40
|
|
34
41
|
|
35
42
|
class ProcessedMarket(BaseModel):
|
36
|
-
answer:
|
43
|
+
answer: CategoricalProbabilisticAnswer
|
37
44
|
|
38
45
|
|
39
46
|
class ProcessedTradedMarket(ProcessedMarket):
|
@@ -66,15 +73,32 @@ class AgentMarket(BaseModel):
|
|
66
73
|
question: str
|
67
74
|
description: str | None
|
68
75
|
outcomes: t.Sequence[OutcomeStr]
|
69
|
-
outcome_token_pool: dict[
|
76
|
+
outcome_token_pool: dict[OutcomeStr, OutcomeToken] | None
|
70
77
|
resolution: Resolution | None
|
71
78
|
created_time: DatetimeUTC | None
|
72
79
|
close_time: DatetimeUTC | None
|
73
|
-
|
80
|
+
|
81
|
+
probabilities: dict[OutcomeStr, Probability]
|
74
82
|
url: str
|
75
83
|
volume: CollateralToken | None
|
76
84
|
fees: MarketFees
|
77
85
|
|
86
|
+
@field_validator("probabilities")
|
87
|
+
def validate_probabilities(
|
88
|
+
cls,
|
89
|
+
probs: dict[OutcomeStr, Probability],
|
90
|
+
info: FieldValidationInfo,
|
91
|
+
) -> dict[OutcomeStr, Probability]:
|
92
|
+
outcomes: t.Sequence[OutcomeStr] = check_not_none(info.data.get("outcomes"))
|
93
|
+
if set(probs.keys()) != set(outcomes):
|
94
|
+
raise ValueError("Keys of `probabilities` must match `outcomes` exactly.")
|
95
|
+
total = float(sum(probs.values()))
|
96
|
+
if not 0.999 <= total <= 1.001:
|
97
|
+
# We simply log a warning because for some use-cases (e.g. existing positions), the
|
98
|
+
# markets might be already closed hence no reliable outcome token prices exist anymore.
|
99
|
+
logger.warning(f"Probabilities for market {info.data=} do not sum to 1.")
|
100
|
+
return probs
|
101
|
+
|
78
102
|
@field_validator("outcome_token_pool")
|
79
103
|
def validate_outcome_token_pool(
|
80
104
|
cls,
|
@@ -91,6 +115,14 @@ class AgentMarket(BaseModel):
|
|
91
115
|
)
|
92
116
|
return outcome_token_pool
|
93
117
|
|
118
|
+
def get_outcome_token_pool_by_outcome(self, outcome: OutcomeStr) -> OutcomeToken:
|
119
|
+
if self.outcome_token_pool is None or not self.outcome_token_pool:
|
120
|
+
return OutcomeToken(0)
|
121
|
+
|
122
|
+
# We look up by index to avoid having to deal with case sensitivity issues.
|
123
|
+
outcome_idx = self.get_outcome_index(outcome)
|
124
|
+
return list(self.outcome_token_pool.values())[outcome_idx]
|
125
|
+
|
94
126
|
@model_validator(mode="before")
|
95
127
|
def handle_legacy_fee(cls, data: dict[str, t.Any]) -> dict[str, t.Any]:
|
96
128
|
# Backward compatibility for older `AgentMarket` without `fees`.
|
@@ -99,33 +131,51 @@ class AgentMarket(BaseModel):
|
|
99
131
|
del data["fee"]
|
100
132
|
return data
|
101
133
|
|
102
|
-
|
103
|
-
|
104
|
-
|
134
|
+
def market_outcome_for_probability_key(
|
135
|
+
self, probability_key: OutcomeStr
|
136
|
+
) -> OutcomeStr:
|
137
|
+
for market_outcome in self.outcomes:
|
138
|
+
if market_outcome.lower() == probability_key.lower():
|
139
|
+
return market_outcome
|
140
|
+
raise ValueError(
|
141
|
+
f"Could not find probability for probability key {probability_key}"
|
142
|
+
)
|
143
|
+
|
144
|
+
def probability_for_market_outcome(self, market_outcome: OutcomeStr) -> Probability:
|
145
|
+
for k, v in self.probabilities.items():
|
146
|
+
if k.lower() == market_outcome.lower():
|
147
|
+
return v
|
148
|
+
raise ValueError(
|
149
|
+
f"Could not find probability for market outcome {market_outcome}"
|
150
|
+
)
|
105
151
|
|
106
152
|
@property
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
"""
|
112
|
-
return CollateralToken(self.current_p_yes)
|
153
|
+
def is_binary(self) -> bool:
|
154
|
+
# 3 outcomes can also be binary if 3rd outcome is invalid (Seer)
|
155
|
+
if len(self.outcomes) not in [2, 3]:
|
156
|
+
return False
|
113
157
|
|
114
|
-
|
115
|
-
|
116
|
-
|
158
|
+
lowercase_outcomes = [outcome.lower() for outcome in self.outcomes]
|
159
|
+
|
160
|
+
has_yes = YES_OUTCOME_LOWERCASE_IDENTIFIER in lowercase_outcomes
|
161
|
+
has_no = NO_OUTCOME_LOWERCASE_IDENTIFIER in lowercase_outcomes
|
162
|
+
|
163
|
+
if len(lowercase_outcomes) == 3:
|
164
|
+
invalid_outcome = lowercase_outcomes[-1]
|
165
|
+
has_invalid = INVALID_OUTCOME_LOWERCASE_IDENTIFIER in invalid_outcome
|
166
|
+
return has_yes and has_no and has_invalid
|
167
|
+
|
168
|
+
return has_yes and has_no
|
117
169
|
|
118
170
|
@property
|
119
|
-
def
|
120
|
-
|
121
|
-
|
122
|
-
Keep as an extra property, in case it wouldn't be true for some prediction market platform.
|
123
|
-
"""
|
124
|
-
return CollateralToken(self.current_p_no)
|
171
|
+
def p_yes(self) -> Probability:
|
172
|
+
probs_lowercase = {o.lower(): p for o, p in self.probabilities.items()}
|
173
|
+
return check_not_none(probs_lowercase.get(YES_OUTCOME_LOWERCASE_IDENTIFIER))
|
125
174
|
|
126
175
|
@property
|
127
|
-
def
|
128
|
-
|
176
|
+
def p_no(self) -> Probability:
|
177
|
+
probs_lowercase = {o.lower(): p for o, p in self.probabilities.items()}
|
178
|
+
return check_not_none(probs_lowercase.get(NO_OUTCOME_LOWERCASE_IDENTIFIER))
|
129
179
|
|
130
180
|
@property
|
131
181
|
def probable_resolution(self) -> Resolution:
|
@@ -135,16 +185,8 @@ class AgentMarket(BaseModel):
|
|
135
185
|
else:
|
136
186
|
raise ValueError(f"Unknown resolution: {self.resolution}")
|
137
187
|
else:
|
138
|
-
|
139
|
-
|
140
|
-
@property
|
141
|
-
def boolean_outcome(self) -> bool:
|
142
|
-
if self.resolution:
|
143
|
-
if self.resolution == Resolution.YES:
|
144
|
-
return True
|
145
|
-
elif self.resolution == Resolution.NO:
|
146
|
-
return False
|
147
|
-
should_not_happen(f"Market {self.id} does not have a successful resolution.")
|
188
|
+
outcome = get_most_probable_outcome(self.probabilities)
|
189
|
+
return Resolution(outcome=outcome, invalid=False)
|
148
190
|
|
149
191
|
def get_last_trade_p_yes(self) -> Probability | None:
|
150
192
|
"""
|
@@ -201,7 +243,7 @@ class AgentMarket(BaseModel):
|
|
201
243
|
raise NotImplementedError("Subclasses must implement this method")
|
202
244
|
|
203
245
|
def get_sell_value_of_outcome_token(
|
204
|
-
self, outcome:
|
246
|
+
self, outcome: OutcomeStr, amount: OutcomeToken
|
205
247
|
) -> CollateralToken:
|
206
248
|
"""
|
207
249
|
When you hold OutcomeToken(s), it's easy to calculate how much you get at the end if you win (1 OutcomeToken will equal to 1 Token).
|
@@ -225,30 +267,77 @@ class AgentMarket(BaseModel):
|
|
225
267
|
"""
|
226
268
|
raise NotImplementedError("Subclasses must implement this method")
|
227
269
|
|
228
|
-
def liquidate_existing_positions(self, outcome:
|
270
|
+
def liquidate_existing_positions(self, outcome: OutcomeStr) -> None:
|
229
271
|
raise NotImplementedError("Subclasses must implement this method")
|
230
272
|
|
231
|
-
def place_bet(self, outcome:
|
273
|
+
def place_bet(self, outcome: OutcomeStr, amount: USD) -> str:
|
232
274
|
raise NotImplementedError("Subclasses must implement this method")
|
233
275
|
|
234
|
-
def buy_tokens(self, outcome:
|
276
|
+
def buy_tokens(self, outcome: OutcomeStr, amount: USD) -> str:
|
235
277
|
return self.place_bet(outcome=outcome, amount=amount)
|
236
278
|
|
237
279
|
def get_buy_token_amount(
|
238
|
-
self, bet_amount: USD | CollateralToken,
|
280
|
+
self, bet_amount: USD | CollateralToken, outcome: OutcomeStr
|
239
281
|
) -> OutcomeToken | None:
|
240
282
|
raise NotImplementedError("Subclasses must implement this method")
|
241
283
|
|
242
|
-
def sell_tokens(self, outcome:
|
284
|
+
def sell_tokens(self, outcome: OutcomeStr, amount: USD | OutcomeToken) -> str:
|
243
285
|
raise NotImplementedError("Subclasses must implement this method")
|
244
286
|
|
245
287
|
@staticmethod
|
246
|
-
def
|
288
|
+
def compute_fpmm_probabilities(balances: list[OutcomeWei]) -> list[Probability]:
|
289
|
+
"""
|
290
|
+
Compute the implied probabilities in a Fixed Product Market Maker.
|
291
|
+
|
292
|
+
Args:
|
293
|
+
balances (List[float]): Balances of outcome tokens.
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
List[float]: Implied probabilities for each outcome.
|
297
|
+
"""
|
298
|
+
if all(x.value == 0 for x in balances):
|
299
|
+
return [Probability(0.0)] * len(balances)
|
300
|
+
|
301
|
+
# converting to standard values for prod compatibility.
|
302
|
+
values_balance = [i.value for i in balances]
|
303
|
+
# Compute product of balances excluding each outcome
|
304
|
+
excluded_products = []
|
305
|
+
for i in range(len(values_balance)):
|
306
|
+
other_balances = values_balance[:i] + values_balance[i + 1 :]
|
307
|
+
excluded_products.append(prod(other_balances))
|
308
|
+
|
309
|
+
# Normalize to sum to 1
|
310
|
+
total = sum(excluded_products)
|
311
|
+
if total == 0:
|
312
|
+
return [Probability(0.0)] * len(balances)
|
313
|
+
probabilities = [Probability(p / total) for p in excluded_products]
|
314
|
+
|
315
|
+
return probabilities
|
316
|
+
|
317
|
+
@staticmethod
|
318
|
+
def build_probability_map_from_p_yes(
|
319
|
+
p_yes: Probability,
|
320
|
+
) -> dict[OutcomeStr, Probability]:
|
321
|
+
return {
|
322
|
+
OutcomeStr(YES_OUTCOME_LOWERCASE_IDENTIFIER): p_yes,
|
323
|
+
OutcomeStr(NO_OUTCOME_LOWERCASE_IDENTIFIER): Probability(1.0 - p_yes),
|
324
|
+
}
|
325
|
+
|
326
|
+
@staticmethod
|
327
|
+
def build_probability_map(
|
328
|
+
outcome_token_amounts: list[OutcomeWei], outcomes: list[OutcomeStr]
|
329
|
+
) -> dict[OutcomeStr, Probability]:
|
330
|
+
probs = AgentMarket.compute_fpmm_probabilities(outcome_token_amounts)
|
331
|
+
return {outcome: prob for outcome, prob in zip(outcomes, probs)}
|
332
|
+
|
333
|
+
@staticmethod
|
334
|
+
def get_markets(
|
247
335
|
limit: int,
|
248
336
|
sort_by: SortBy,
|
249
337
|
filter_by: FilterBy = FilterBy.OPEN,
|
250
338
|
created_after: t.Optional[DatetimeUTC] = None,
|
251
339
|
excluded_questions: set[str] | None = None,
|
340
|
+
fetch_categorical_markets: bool = False,
|
252
341
|
) -> t.Sequence["AgentMarket"]:
|
253
342
|
raise NotImplementedError("Subclasses must implement this method")
|
254
343
|
|
@@ -328,12 +417,17 @@ class AgentMarket(BaseModel):
|
|
328
417
|
return self.get_liquidity() > 0
|
329
418
|
|
330
419
|
def has_successful_resolution(self) -> bool:
|
331
|
-
return
|
420
|
+
return (
|
421
|
+
self.resolution is not None
|
422
|
+
and self.resolution.outcome is not None
|
423
|
+
and not self.resolution.invalid
|
424
|
+
)
|
332
425
|
|
333
426
|
def has_unsuccessful_resolution(self) -> bool:
|
334
|
-
return self.resolution
|
427
|
+
return self.resolution is not None and self.resolution.invalid
|
335
428
|
|
336
|
-
|
429
|
+
@staticmethod
|
430
|
+
def get_outcome_str_from_bool(outcome: bool) -> OutcomeStr:
|
337
431
|
raise NotImplementedError("Subclasses must implement this method")
|
338
432
|
|
339
433
|
def get_outcome_str(self, outcome_index: int) -> OutcomeStr:
|
@@ -344,13 +438,14 @@ class AgentMarket(BaseModel):
|
|
344
438
|
f"Outcome index `{outcome_index}` out of range for `{self.outcomes}`: `{self.outcomes}`."
|
345
439
|
)
|
346
440
|
|
347
|
-
def get_outcome_index(self, outcome:
|
441
|
+
def get_outcome_index(self, outcome: OutcomeStr) -> int:
|
442
|
+
outcomes_lowercase = [o.lower() for o in self.outcomes]
|
348
443
|
try:
|
349
|
-
return
|
444
|
+
return outcomes_lowercase.index(outcome.lower())
|
350
445
|
except ValueError:
|
351
446
|
raise ValueError(f"Outcome `{outcome}` not found in `{self.outcomes}`.")
|
352
447
|
|
353
|
-
def get_token_balance(self, user_id: str, outcome:
|
448
|
+
def get_token_balance(self, user_id: str, outcome: OutcomeStr) -> OutcomeToken:
|
354
449
|
raise NotImplementedError("Subclasses must implement this method")
|
355
450
|
|
356
451
|
def get_position(self, user_id: str) -> ExistingPosition | None:
|
@@ -385,7 +480,7 @@ class AgentMarket(BaseModel):
|
|
385
480
|
def has_token_pool(self) -> bool:
|
386
481
|
return self.outcome_token_pool is not None
|
387
482
|
|
388
|
-
def get_pool_tokens(self, outcome:
|
483
|
+
def get_pool_tokens(self, outcome: OutcomeStr) -> OutcomeToken:
|
389
484
|
if not self.outcome_token_pool:
|
390
485
|
raise ValueError("Outcome token pool is not available.")
|
391
486
|
|
@@ -16,6 +16,9 @@ from prediction_market_agent_tooling.tools.ipfs.ipfs_handler import IPFSHandler
|
|
16
16
|
from prediction_market_agent_tooling.tools.utils import BPS_CONSTANT
|
17
17
|
from prediction_market_agent_tooling.tools.web3_utils import ipfscidv0_to_byte32
|
18
18
|
|
19
|
+
# max uint16 for easy prediction identification (if market does not have YES outcome)
|
20
|
+
UINT16_MAX = 2**16 - 1 # = 65535
|
21
|
+
|
19
22
|
|
20
23
|
def store_trades(
|
21
24
|
market_id: str,
|
@@ -28,6 +31,10 @@ def store_trades(
|
|
28
31
|
logger.warning(f"No prediction for market {market_id}, not storing anything.")
|
29
32
|
return None
|
30
33
|
|
34
|
+
yes_probability = traded_market.answer.get_yes_probability()
|
35
|
+
if not yes_probability:
|
36
|
+
logger.info("Skipping this since no yes_probability available.")
|
37
|
+
return None
|
31
38
|
reasoning = traded_market.answer.reasoning if traded_market.answer.reasoning else ""
|
32
39
|
|
33
40
|
ipfs_hash_decoded = HexBytes(HASH_ZERO)
|
@@ -43,11 +50,13 @@ def store_trades(
|
|
43
50
|
HexBytes(HexStr(i.id)) for i in traded_market.trades if i.id is not None
|
44
51
|
]
|
45
52
|
|
53
|
+
estimated_probability_bps = int(yes_probability * BPS_CONSTANT)
|
54
|
+
|
46
55
|
prediction = ContractPrediction(
|
47
56
|
publisher=keys.bet_from_address,
|
48
57
|
ipfs_hash=ipfs_hash_decoded,
|
49
58
|
tx_hashes=tx_hashes,
|
50
|
-
estimated_probability_bps=
|
59
|
+
estimated_probability_bps=estimated_probability_bps,
|
51
60
|
)
|
52
61
|
tx_receipt = OmenAgentResultMappingContract().add_prediction(
|
53
62
|
api_keys=keys,
|
@@ -3,6 +3,10 @@ from typing import Annotated
|
|
3
3
|
|
4
4
|
from pydantic import BaseModel, BeforeValidator, computed_field
|
5
5
|
|
6
|
+
from prediction_market_agent_tooling.deploy.constants import (
|
7
|
+
NO_OUTCOME_LOWERCASE_IDENTIFIER,
|
8
|
+
YES_OUTCOME_LOWERCASE_IDENTIFIER,
|
9
|
+
)
|
6
10
|
from prediction_market_agent_tooling.gtypes import (
|
7
11
|
USD,
|
8
12
|
CollateralToken,
|
@@ -11,24 +15,22 @@ from prediction_market_agent_tooling.gtypes import (
|
|
11
15
|
Probability,
|
12
16
|
)
|
13
17
|
from prediction_market_agent_tooling.logprobs_parser import FieldLogprobs
|
14
|
-
from prediction_market_agent_tooling.tools.utils import DatetimeUTC
|
18
|
+
from prediction_market_agent_tooling.tools.utils import DatetimeUTC, check_not_none
|
15
19
|
|
16
20
|
|
17
|
-
class Resolution(
|
18
|
-
|
19
|
-
|
20
|
-
CANCEL = "CANCEL"
|
21
|
-
MKT = "MKT"
|
21
|
+
class Resolution(BaseModel):
|
22
|
+
outcome: OutcomeStr | None
|
23
|
+
invalid: bool
|
22
24
|
|
23
25
|
@staticmethod
|
24
|
-
def
|
25
|
-
return Resolution
|
26
|
+
def from_answer(answer: OutcomeStr) -> "Resolution":
|
27
|
+
return Resolution(outcome=answer, invalid=False)
|
26
28
|
|
27
29
|
|
28
30
|
class Bet(BaseModel):
|
29
31
|
id: str
|
30
32
|
amount: CollateralToken
|
31
|
-
outcome:
|
33
|
+
outcome: OutcomeStr
|
32
34
|
created_time: DatetimeUTC
|
33
35
|
market_question: str
|
34
36
|
market_id: str
|
@@ -38,7 +40,7 @@ class Bet(BaseModel):
|
|
38
40
|
|
39
41
|
|
40
42
|
class ResolvedBet(Bet):
|
41
|
-
market_outcome:
|
43
|
+
market_outcome: OutcomeStr
|
42
44
|
resolved_time: DatetimeUTC
|
43
45
|
profit: CollateralToken
|
44
46
|
|
@@ -84,6 +86,72 @@ class ProbabilisticAnswer(BaseModel):
|
|
84
86
|
def p_no(self) -> Probability:
|
85
87
|
return Probability(1 - self.p_yes)
|
86
88
|
|
89
|
+
@property
|
90
|
+
def probable_resolution(self) -> Resolution:
|
91
|
+
return (
|
92
|
+
Resolution(
|
93
|
+
outcome=OutcomeStr(YES_OUTCOME_LOWERCASE_IDENTIFIER), invalid=False
|
94
|
+
)
|
95
|
+
if self.p_yes > 0.5
|
96
|
+
else Resolution(
|
97
|
+
outcome=OutcomeStr(NO_OUTCOME_LOWERCASE_IDENTIFIER), invalid=False
|
98
|
+
)
|
99
|
+
)
|
100
|
+
|
101
|
+
|
102
|
+
class CategoricalProbabilisticAnswer(BaseModel):
|
103
|
+
probabilities: dict[OutcomeStr, Probability]
|
104
|
+
confidence: float
|
105
|
+
reasoning: str | None = None
|
106
|
+
|
107
|
+
@property
|
108
|
+
def probable_resolution(self) -> Resolution:
|
109
|
+
most_likely_outcome = max(
|
110
|
+
self.probabilities.items(),
|
111
|
+
key=lambda item: item[1],
|
112
|
+
)[0]
|
113
|
+
return Resolution(outcome=most_likely_outcome, invalid=False)
|
114
|
+
|
115
|
+
def to_probabilistic_answer(self) -> ProbabilisticAnswer:
|
116
|
+
p_yes = check_not_none(self.get_yes_probability())
|
117
|
+
return ProbabilisticAnswer(
|
118
|
+
p_yes=p_yes,
|
119
|
+
confidence=self.confidence,
|
120
|
+
)
|
121
|
+
|
122
|
+
@staticmethod
|
123
|
+
def from_probabilistic_answer(
|
124
|
+
answer: ProbabilisticAnswer,
|
125
|
+
) -> "CategoricalProbabilisticAnswer":
|
126
|
+
return CategoricalProbabilisticAnswer(
|
127
|
+
probabilities={
|
128
|
+
OutcomeStr(YES_OUTCOME_LOWERCASE_IDENTIFIER): answer.p_yes,
|
129
|
+
OutcomeStr(NO_OUTCOME_LOWERCASE_IDENTIFIER): Probability(
|
130
|
+
1 - answer.p_yes
|
131
|
+
),
|
132
|
+
},
|
133
|
+
confidence=answer.confidence,
|
134
|
+
reasoning=answer.reasoning,
|
135
|
+
)
|
136
|
+
|
137
|
+
def probability_for_market_outcome(self, market_outcome: OutcomeStr) -> Probability:
|
138
|
+
for k, v in self.probabilities.items():
|
139
|
+
if k.lower() == market_outcome.lower():
|
140
|
+
return v
|
141
|
+
raise ValueError(
|
142
|
+
f"Could not find probability for market outcome {market_outcome}"
|
143
|
+
)
|
144
|
+
|
145
|
+
def get_yes_probability(self) -> Probability | None:
|
146
|
+
return next(
|
147
|
+
(
|
148
|
+
p
|
149
|
+
for o, p in self.probabilities.items()
|
150
|
+
if o.lower() == YES_OUTCOME_LOWERCASE_IDENTIFIER
|
151
|
+
),
|
152
|
+
None,
|
153
|
+
)
|
154
|
+
|
87
155
|
|
88
156
|
class Position(BaseModel):
|
89
157
|
market_id: str
|
@@ -124,7 +192,7 @@ class TradeType(str, Enum):
|
|
124
192
|
|
125
193
|
class Trade(BaseModel):
|
126
194
|
trade_type: TradeType
|
127
|
-
outcome:
|
195
|
+
outcome: OutcomeStr
|
128
196
|
amount: USD
|
129
197
|
|
130
198
|
|
@@ -144,13 +212,13 @@ class PlacedTrade(Trade):
|
|
144
212
|
class SimulatedBetDetail(BaseModel):
|
145
213
|
strategy: str
|
146
214
|
url: str
|
147
|
-
|
148
|
-
|
215
|
+
probabilities: dict[OutcomeStr, Probability]
|
216
|
+
agent_prob_multi: dict[OutcomeStr, Probability]
|
149
217
|
agent_conf: float
|
150
218
|
org_bet: CollateralToken
|
151
219
|
sim_bet: CollateralToken
|
152
|
-
org_dir:
|
153
|
-
sim_dir:
|
220
|
+
org_dir: OutcomeStr
|
221
|
+
sim_dir: OutcomeStr
|
154
222
|
org_profit: CollateralToken
|
155
223
|
sim_profit: CollateralToken
|
156
224
|
timestamp: DatetimeUTC
|
@@ -170,6 +238,4 @@ class SimulatedLifetimeDetail(BaseModel):
|
|
170
238
|
total_simulated_profit: CollateralToken
|
171
239
|
roi: float
|
172
240
|
simulated_roi: float
|
173
|
-
sharpe_output_original: SharpeOutput
|
174
|
-
sharpe_output_simulation: SharpeOutput
|
175
241
|
maximize: float
|
@@ -3,9 +3,9 @@ import typing as t
|
|
3
3
|
import requests
|
4
4
|
import tenacity
|
5
5
|
|
6
|
-
from prediction_market_agent_tooling.gtypes import Mana, SecretStr
|
6
|
+
from prediction_market_agent_tooling.gtypes import Mana, OutcomeStr, SecretStr
|
7
7
|
from prediction_market_agent_tooling.loggers import logger
|
8
|
-
from prediction_market_agent_tooling.markets.data_models import ResolvedBet
|
8
|
+
from prediction_market_agent_tooling.markets.data_models import Resolution, ResolvedBet
|
9
9
|
from prediction_market_agent_tooling.markets.manifold.data_models import (
|
10
10
|
FullManifoldMarket,
|
11
11
|
ManifoldBet,
|
@@ -105,14 +105,13 @@ def get_one_manifold_binary_market() -> ManifoldMarket:
|
|
105
105
|
after=lambda x: logger.debug(f"place_bet failed, {x.attempt_number=}."),
|
106
106
|
)
|
107
107
|
def place_bet(
|
108
|
-
amount: Mana, market_id: str, outcome:
|
108
|
+
amount: Mana, market_id: str, outcome: OutcomeStr, manifold_api_key: SecretStr
|
109
109
|
) -> ManifoldBet:
|
110
|
-
outcome_str = "YES" if outcome else "NO"
|
111
110
|
url = f"{MANIFOLD_API_BASE_URL}/v0/bet"
|
112
111
|
params = {
|
113
112
|
"amount": float(amount), # Convert to float to avoid serialization issues.
|
114
113
|
"contractId": market_id,
|
115
|
-
"outcome":
|
114
|
+
"outcome": outcome,
|
116
115
|
}
|
117
116
|
|
118
117
|
headers = {
|
@@ -204,11 +203,12 @@ def manifold_to_generic_resolved_bet(
|
|
204
203
|
if not market.resolutionTime:
|
205
204
|
raise ValueError(f"Market {market.id} has no resolution time.")
|
206
205
|
|
207
|
-
market_outcome = market.
|
206
|
+
market_outcome = market.get_resolved_outcome()
|
207
|
+
|
208
208
|
return ResolvedBet(
|
209
209
|
id=bet.id,
|
210
210
|
amount=bet.amount,
|
211
|
-
outcome=bet.
|
211
|
+
outcome=bet.get_resolved_outcome(),
|
212
212
|
created_time=bet.createdTime,
|
213
213
|
market_question=market.question,
|
214
214
|
market_id=market.id,
|
@@ -224,3 +224,14 @@ def get_market_positions(market_id: str, user_id: str) -> list[ManifoldContractM
|
|
224
224
|
return response_list_to_model(
|
225
225
|
requests.get(url, params=params), ManifoldContractMetric
|
226
226
|
)
|
227
|
+
|
228
|
+
|
229
|
+
def find_resolution_on_manifold(question: str, n: int = 100) -> Resolution | None:
|
230
|
+
# Even with exact-match search, Manifold doesn't return it as the first result, increase `n` if you can't find market that you know exists.
|
231
|
+
manifold_markets = get_manifold_binary_markets(
|
232
|
+
n, term=question, filter_=None, sort=None
|
233
|
+
)
|
234
|
+
for manifold_market in manifold_markets:
|
235
|
+
if manifold_market.question == question:
|
236
|
+
return manifold_market.resolution
|
237
|
+
return None
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import typing as t
|
2
2
|
from enum import Enum
|
3
3
|
|
4
|
-
from pydantic import BaseModel
|
4
|
+
from pydantic import BaseModel, field_validator
|
5
5
|
|
6
6
|
from prediction_market_agent_tooling.gtypes import (
|
7
7
|
USD,
|
@@ -12,6 +12,7 @@ from prediction_market_agent_tooling.gtypes import (
|
|
12
12
|
Probability,
|
13
13
|
)
|
14
14
|
from prediction_market_agent_tooling.markets.data_models import Resolution
|
15
|
+
from prediction_market_agent_tooling.markets.manifold.utils import validate_resolution
|
15
16
|
from prediction_market_agent_tooling.tools.utils import DatetimeUTC, should_not_happen
|
16
17
|
|
17
18
|
MANIFOLD_BASE_URL = "https://manifold.markets"
|
@@ -92,21 +93,25 @@ class ManifoldMarket(BaseModel):
|
|
92
93
|
def outcomes(self) -> t.Sequence[OutcomeStr]:
|
93
94
|
return [OutcomeStr(o) for o in self.pool.model_fields.keys()]
|
94
95
|
|
95
|
-
def
|
96
|
-
if self.resolution
|
97
|
-
return
|
98
|
-
elif self.resolution == Resolution.NO:
|
99
|
-
return False
|
96
|
+
def get_resolved_outcome(self) -> OutcomeStr:
|
97
|
+
if self.resolution and self.resolution.outcome:
|
98
|
+
return self.resolution.outcome
|
100
99
|
else:
|
101
|
-
|
100
|
+
raise ValueError(f"Market is not resolved. Resolution {self.resolution=}")
|
102
101
|
|
103
102
|
def is_resolved_non_cancelled(self) -> bool:
|
104
103
|
return (
|
105
104
|
self.isResolved
|
106
105
|
and self.resolutionTime is not None
|
107
|
-
and self.resolution not
|
106
|
+
and self.resolution is not None
|
107
|
+
and self.resolution.outcome is not None
|
108
|
+
and not self.resolution.invalid
|
108
109
|
)
|
109
110
|
|
111
|
+
@field_validator("resolution", mode="before")
|
112
|
+
def validate_resolution(cls, v: t.Any) -> Resolution:
|
113
|
+
return validate_resolution(v)
|
114
|
+
|
110
115
|
def __repr__(self) -> str:
|
111
116
|
return f"Manifold's market: {self.question}"
|
112
117
|
|
@@ -197,18 +202,20 @@ class ManifoldBet(BaseModel):
|
|
197
202
|
createdTime: DatetimeUTC
|
198
203
|
outcome: Resolution
|
199
204
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
+
@field_validator("outcome", mode="before")
|
206
|
+
def validate_resolution(cls, v: t.Any) -> Resolution:
|
207
|
+
return validate_resolution(v)
|
208
|
+
|
209
|
+
def get_resolved_outcome(self) -> OutcomeStr:
|
210
|
+
if self.outcome.outcome:
|
211
|
+
return self.outcome.outcome
|
205
212
|
else:
|
206
|
-
|
213
|
+
raise ValueError(f"Bet {self.id} is not resolved. {self.outcome=}")
|
207
214
|
|
208
|
-
def get_profit(self, market_outcome:
|
215
|
+
def get_profit(self, market_outcome: OutcomeStr) -> CollateralToken:
|
209
216
|
profit = (
|
210
217
|
self.shares - self.amount
|
211
|
-
if self.
|
218
|
+
if self.get_resolved_outcome() == market_outcome
|
212
219
|
else -self.amount
|
213
220
|
)
|
214
221
|
return profit
|