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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import streamlit
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
from prediction_market_agent_tooling.config import APIKeys
|
|
9
|
+
from prediction_market_agent_tooling.loggers import logger
|
|
10
|
+
from prediction_market_agent_tooling.tools.caches.db_cache import DB_CACHE_LOG_PREFIX
|
|
11
|
+
|
|
12
|
+
if t.TYPE_CHECKING:
|
|
13
|
+
from loguru import Message
|
|
14
|
+
|
|
15
|
+
STREAMLIT_SINK_EXPLICIT_FLAG = "streamlit"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def streamlit_asyncio_event_loop_hack() -> None:
|
|
19
|
+
"""
|
|
20
|
+
This function is a hack to make Streamlit work with asyncio event loop.
|
|
21
|
+
See https://github.com/streamlit/streamlit/issues/744
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def get_or_create_eventloop() -> asyncio.AbstractEventLoop:
|
|
25
|
+
try:
|
|
26
|
+
return asyncio.get_event_loop()
|
|
27
|
+
except RuntimeError as ex:
|
|
28
|
+
if "There is no current event loop in thread" in str(ex):
|
|
29
|
+
loop = asyncio.new_event_loop()
|
|
30
|
+
asyncio.set_event_loop(loop)
|
|
31
|
+
return asyncio.get_event_loop()
|
|
32
|
+
else:
|
|
33
|
+
raise ex
|
|
34
|
+
|
|
35
|
+
loop = get_or_create_eventloop()
|
|
36
|
+
asyncio.set_event_loop(loop)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_required_api_keys(keys: APIKeys, required_keys: list[str]) -> None:
|
|
40
|
+
has_missing_keys = False
|
|
41
|
+
for key in required_keys:
|
|
42
|
+
if not getattr(keys, key):
|
|
43
|
+
st.error(f"Environment variable for key {key} has not been set.")
|
|
44
|
+
has_missing_keys = True
|
|
45
|
+
if has_missing_keys:
|
|
46
|
+
st.stop()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_loguru_streamlit_sink(
|
|
50
|
+
explicit: bool,
|
|
51
|
+
expander_if_longer_than: int | None,
|
|
52
|
+
include_in_expander: int = 50,
|
|
53
|
+
) -> t.Callable[["Message"], None]:
|
|
54
|
+
def loguru_streamlit_sink(log: "Message") -> None:
|
|
55
|
+
record = log.record
|
|
56
|
+
level = record["level"].name
|
|
57
|
+
|
|
58
|
+
message = streamlit_escape(record["message"])
|
|
59
|
+
|
|
60
|
+
# Ignore certain messages that aren't interesting for Streamlit user, but are in the production logs.
|
|
61
|
+
if any(x in message for x in [DB_CACHE_LOG_PREFIX]):
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if explicit and not record["extra"].get(STREAMLIT_SINK_EXPLICIT_FLAG, False):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if level == "ERROR":
|
|
68
|
+
st_func = st.error
|
|
69
|
+
st_icon = "❌"
|
|
70
|
+
|
|
71
|
+
elif level == "WARNING":
|
|
72
|
+
st_func = st.warning
|
|
73
|
+
st_icon = "⚠️"
|
|
74
|
+
|
|
75
|
+
elif level == "SUCCESS":
|
|
76
|
+
st_func = st.success
|
|
77
|
+
st_icon = "✅"
|
|
78
|
+
|
|
79
|
+
elif level == "DEBUG":
|
|
80
|
+
st_func = None
|
|
81
|
+
st_icon = None
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
st_func = st.info
|
|
85
|
+
st_icon = "ℹ️"
|
|
86
|
+
|
|
87
|
+
if st_func is None:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
elif (
|
|
91
|
+
expander_if_longer_than is not None
|
|
92
|
+
and len(message) > expander_if_longer_than
|
|
93
|
+
):
|
|
94
|
+
with st.expander(
|
|
95
|
+
f"[Expand to see more] {message[:include_in_expander]}..."
|
|
96
|
+
):
|
|
97
|
+
st_func(message, icon=st_icon)
|
|
98
|
+
|
|
99
|
+
else:
|
|
100
|
+
st_func(message, icon=st_icon)
|
|
101
|
+
|
|
102
|
+
return loguru_streamlit_sink
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@st.cache_resource
|
|
106
|
+
def add_sink_to_logger(
|
|
107
|
+
explicit: bool = False, expander_if_longer_than: int | None = 500
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Adds streamlit as a sink to the loguru, so any loguru logs will be shown in the streamlit app.
|
|
111
|
+
|
|
112
|
+
Needs to be behind a cache decorator, so it only runs once per streamlit session (otherwise we would see duplicated messages).
|
|
113
|
+
|
|
114
|
+
If `explicit` is set to True, only logged messages with extra attribute `streamlit` will be shown in the streamlit app.
|
|
115
|
+
"""
|
|
116
|
+
logger.add(
|
|
117
|
+
get_loguru_streamlit_sink(
|
|
118
|
+
explicit=explicit, expander_if_longer_than=expander_if_longer_than
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def streamlit_escape(message: str) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Escapes the string for streamlit writes.
|
|
126
|
+
"""
|
|
127
|
+
# Replace escaped newlines with actual newlines.
|
|
128
|
+
message = message.replace("\\n", "\n")
|
|
129
|
+
# Fix malformed dollar signs in the messages.
|
|
130
|
+
message = message.replace("$", "\$")
|
|
131
|
+
|
|
132
|
+
return message
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def dict_to_point_list(d: dict[str, t.Any], indent: int = 0) -> str:
|
|
136
|
+
"""
|
|
137
|
+
Helper method to convert nested dicts to a bullet point list.
|
|
138
|
+
"""
|
|
139
|
+
lines = []
|
|
140
|
+
prefix = " " * indent
|
|
141
|
+
for k, v in d.items():
|
|
142
|
+
if isinstance(v, dict):
|
|
143
|
+
lines.append(f"{prefix}- {k}:")
|
|
144
|
+
lines.append(dict_to_point_list(v, indent + 1))
|
|
145
|
+
elif isinstance(v, list):
|
|
146
|
+
lines.append(f"{prefix}- {k}:")
|
|
147
|
+
for idx, item in enumerate(v):
|
|
148
|
+
if isinstance(item, dict):
|
|
149
|
+
lines.append(f"{prefix} - item {idx}:")
|
|
150
|
+
lines.append(dict_to_point_list(item, indent + 2))
|
|
151
|
+
else:
|
|
152
|
+
lines.append(f"{prefix} - item {idx}: {item}")
|
|
153
|
+
else:
|
|
154
|
+
lines.append(f"{prefix}- {k}: {v}")
|
|
155
|
+
return "\n".join(lines)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def customize_index_html(head_content: str) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Unfortunatelly, Streamlit doesn't allow to update HTML content of the main index.html file, any component that allows passing of HTML will render it in iframe.
|
|
161
|
+
That's unusable for analytics tools like Posthog.
|
|
162
|
+
|
|
163
|
+
This is workaround that patches their index.html file directly in their package, found in https://stackoverflow.com/questions/70520191/how-to-add-the-google-analytics-tag-to-website-developed-with-streamlit/78992559#78992559.
|
|
164
|
+
|
|
165
|
+
There is also an issue that tracks this feature (open since 2023): https://github.com/streamlit/streamlit/issues/6140
|
|
166
|
+
"""
|
|
167
|
+
streamlit_package_dir = os.path.dirname(streamlit.__file__)
|
|
168
|
+
index_path = os.path.join(streamlit_package_dir, "static", "index.html")
|
|
169
|
+
|
|
170
|
+
with open(index_path, "r") as f:
|
|
171
|
+
index_html = f.read()
|
|
172
|
+
|
|
173
|
+
if head_content not in index_html:
|
|
174
|
+
# Add the custom content to the head
|
|
175
|
+
index_html = index_html.replace("</head>", f"{head_content}</head>")
|
|
176
|
+
|
|
177
|
+
# Replace the <title> tag
|
|
178
|
+
index_html = index_html.replace(
|
|
179
|
+
"<title>Streamlit</title>", "<title>Savantly is cool</title>"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
with open(index_path, "w") as f:
|
|
183
|
+
f.write(index_html)
|
|
184
|
+
|
|
185
|
+
logger.info("index.html injected with custom code.")
|
|
186
|
+
|
|
187
|
+
else:
|
|
188
|
+
logger.info("index.html injection skipped because it's already present.")
|
|
@@ -3,17 +3,28 @@ from web3 import Web3
|
|
|
3
3
|
from prediction_market_agent_tooling.config import APIKeys
|
|
4
4
|
from prediction_market_agent_tooling.gtypes import USD, Wei
|
|
5
5
|
from prediction_market_agent_tooling.loggers import logger
|
|
6
|
+
from prediction_market_agent_tooling.markets.seer.seer_contracts import GnosisRouter
|
|
7
|
+
from prediction_market_agent_tooling.markets.seer.seer_subgraph_handler import (
|
|
8
|
+
SeerSubgraphHandler,
|
|
9
|
+
)
|
|
6
10
|
from prediction_market_agent_tooling.tools.contract import (
|
|
7
11
|
ContractDepositableWrapperERC20BaseClass,
|
|
8
12
|
ContractERC20BaseClass,
|
|
9
13
|
ContractERC20OnGnosisChain,
|
|
10
14
|
ContractERC4626BaseClass,
|
|
15
|
+
ContractWrapped1155BaseClass,
|
|
16
|
+
init_collateral_token_contract,
|
|
17
|
+
to_gnosis_chain_contract,
|
|
11
18
|
)
|
|
12
19
|
from prediction_market_agent_tooling.tools.cow.cow_order import (
|
|
13
20
|
get_sell_token_amount,
|
|
21
|
+
handle_allowance,
|
|
14
22
|
swap_tokens_waiting,
|
|
15
23
|
)
|
|
16
24
|
from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
|
|
25
|
+
from prediction_market_agent_tooling.tools.tokens.slippage import (
|
|
26
|
+
get_slippage_tolerance_per_token,
|
|
27
|
+
)
|
|
17
28
|
from prediction_market_agent_tooling.tools.tokens.usd import get_usd_in_token
|
|
18
29
|
from prediction_market_agent_tooling.tools.utils import should_not_happen
|
|
19
30
|
|
|
@@ -46,6 +57,9 @@ def auto_deposit_collateral_token(
|
|
|
46
57
|
collateral_token_contract, collateral_amount_wei, api_keys, web3
|
|
47
58
|
)
|
|
48
59
|
|
|
60
|
+
elif isinstance(collateral_token_contract, ContractWrapped1155BaseClass):
|
|
61
|
+
mint_full_set(collateral_token_contract, collateral_amount_wei, api_keys, web3)
|
|
62
|
+
|
|
49
63
|
elif isinstance(collateral_token_contract, ContractERC20BaseClass):
|
|
50
64
|
auto_deposit_erc20(
|
|
51
65
|
collateral_token_contract, collateral_amount_wei, api_keys, web3
|
|
@@ -156,10 +170,60 @@ def auto_deposit_erc20(
|
|
|
156
170
|
raise ValueError(
|
|
157
171
|
"Not enough of the source token to sell to get the desired amount of the collateral token."
|
|
158
172
|
)
|
|
173
|
+
slippage_tolerance = get_slippage_tolerance_per_token(
|
|
174
|
+
KEEPING_ERC20_TOKEN.address, collateral_token_contract.address
|
|
175
|
+
)
|
|
159
176
|
swap_tokens_waiting(
|
|
160
177
|
amount_wei=amount_to_sell_wei,
|
|
161
178
|
sell_token=KEEPING_ERC20_TOKEN.address,
|
|
162
179
|
buy_token=collateral_token_contract.address,
|
|
163
180
|
api_keys=api_keys,
|
|
164
181
|
web3=web3,
|
|
182
|
+
slippage_tolerance=slippage_tolerance,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def mint_full_set(
|
|
187
|
+
collateral_token_contract: ContractERC20BaseClass,
|
|
188
|
+
collateral_amount_wei: Wei,
|
|
189
|
+
api_keys: APIKeys,
|
|
190
|
+
web3: Web3 | None,
|
|
191
|
+
) -> None:
|
|
192
|
+
router = GnosisRouter()
|
|
193
|
+
# We need to fetch the parent's market collateral token, to split it and get the collateral token
|
|
194
|
+
# of the child market.
|
|
195
|
+
seer_subgraph_handler = SeerSubgraphHandler()
|
|
196
|
+
market = seer_subgraph_handler.get_market_by_wrapped_token(
|
|
197
|
+
tokens=[collateral_token_contract.address]
|
|
198
|
+
)
|
|
199
|
+
market_collateral_token = Web3.to_checksum_address(market.collateral_token)
|
|
200
|
+
|
|
201
|
+
balance_market_collateral = ContractERC20OnGnosisChain(
|
|
202
|
+
address=market_collateral_token
|
|
203
|
+
).balanceOf(for_address=api_keys.bet_from_address, web3=web3)
|
|
204
|
+
if balance_market_collateral < collateral_amount_wei:
|
|
205
|
+
logger.debug(
|
|
206
|
+
f"Not enough collateral token in the market. Expected {collateral_amount_wei} but got {balance_market_collateral}. Auto-depositing market collateral."
|
|
207
|
+
)
|
|
208
|
+
market_collateral_token_contract = to_gnosis_chain_contract(
|
|
209
|
+
init_collateral_token_contract(market_collateral_token, web3=web3)
|
|
210
|
+
)
|
|
211
|
+
auto_deposit_collateral_token(
|
|
212
|
+
market_collateral_token_contract, collateral_amount_wei, api_keys, web3
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
handle_allowance(
|
|
216
|
+
api_keys=api_keys,
|
|
217
|
+
sell_token=market_collateral_token,
|
|
218
|
+
amount_to_check_wei=collateral_amount_wei,
|
|
219
|
+
for_address=router.address,
|
|
220
|
+
web3=web3,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
router.split_position(
|
|
224
|
+
api_keys=api_keys,
|
|
225
|
+
collateral_token=market_collateral_token,
|
|
226
|
+
market_id=Web3.to_checksum_address(market.id),
|
|
227
|
+
amount=collateral_amount_wei,
|
|
228
|
+
web3=web3,
|
|
165
229
|
)
|
|
@@ -9,6 +9,9 @@ from prediction_market_agent_tooling.tools.contract import (
|
|
|
9
9
|
)
|
|
10
10
|
from prediction_market_agent_tooling.tools.cow.cow_order import swap_tokens_waiting
|
|
11
11
|
from prediction_market_agent_tooling.tools.tokens.main_token import KEEPING_ERC20_TOKEN
|
|
12
|
+
from prediction_market_agent_tooling.tools.tokens.slippage import (
|
|
13
|
+
get_slippage_tolerance_per_token,
|
|
14
|
+
)
|
|
12
15
|
from prediction_market_agent_tooling.tools.utils import should_not_happen
|
|
13
16
|
|
|
14
17
|
|
|
@@ -49,12 +52,17 @@ def auto_withdraw_collateral_token(
|
|
|
49
52
|
f"Swapping {amount_wei.as_token} from {collateral_token_contract.symbol_cached(web3)} into {KEEPING_ERC20_TOKEN.symbol_cached(web3)}"
|
|
50
53
|
)
|
|
51
54
|
# Otherwise, DEX will handle the rest of token swaps.
|
|
55
|
+
slippage_tolerance = get_slippage_tolerance_per_token(
|
|
56
|
+
collateral_token_contract.address,
|
|
57
|
+
KEEPING_ERC20_TOKEN.address,
|
|
58
|
+
)
|
|
52
59
|
swap_tokens_waiting(
|
|
53
60
|
amount_wei=amount_wei,
|
|
54
61
|
sell_token=collateral_token_contract.address,
|
|
55
62
|
buy_token=KEEPING_ERC20_TOKEN.address,
|
|
56
63
|
api_keys=api_keys,
|
|
57
64
|
web3=web3,
|
|
65
|
+
slippage_tolerance=slippage_tolerance,
|
|
58
66
|
)
|
|
59
67
|
else:
|
|
60
68
|
should_not_happen("Unsupported ERC20 contract type.")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from prediction_market_agent_tooling.gtypes import ChecksumAddress
|
|
2
|
+
from prediction_market_agent_tooling.markets.omen.omen_constants import (
|
|
3
|
+
METRI_SUPER_GROUP_CONTRACT_ADDRESS,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
DEFAULT_SLIPPAGE_TOLERANCE = 0.05
|
|
7
|
+
|
|
8
|
+
SLIPPAGE_TOLERANCE_PER_TOKEN = {
|
|
9
|
+
METRI_SUPER_GROUP_CONTRACT_ADDRESS: 0.1,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_slippage_tolerance_per_token(
|
|
14
|
+
sell_token: ChecksumAddress,
|
|
15
|
+
buy_token: ChecksumAddress,
|
|
16
|
+
default_slippage: float = DEFAULT_SLIPPAGE_TOLERANCE,
|
|
17
|
+
) -> float:
|
|
18
|
+
return max(
|
|
19
|
+
SLIPPAGE_TOLERANCE_PER_TOKEN.get(sell_token, default_slippage),
|
|
20
|
+
SLIPPAGE_TOLERANCE_PER_TOKEN.get(buy_token, default_slippage),
|
|
21
|
+
)
|
|
@@ -10,6 +10,9 @@ from prediction_market_agent_tooling.markets.omen.omen_constants import (
|
|
|
10
10
|
SDAI_CONTRACT_ADDRESS,
|
|
11
11
|
WRAPPED_XDAI_CONTRACT_ADDRESS,
|
|
12
12
|
)
|
|
13
|
+
from prediction_market_agent_tooling.markets.polymarket.polymarket_contracts import (
|
|
14
|
+
USDCeContract,
|
|
15
|
+
)
|
|
13
16
|
from prediction_market_agent_tooling.tools.contract import ContractERC4626OnGnosisChain
|
|
14
17
|
from prediction_market_agent_tooling.tools.cow.cow_order import (
|
|
15
18
|
get_buy_token_amount_else_raise,
|
|
@@ -39,8 +42,8 @@ def get_token_in_usd(amount: CollateralToken, token_address: ChecksumAddress) ->
|
|
|
39
42
|
# A short cache to not spam CoW and prevent timeouts, but still have relatively fresh data.
|
|
40
43
|
@cached(TTLCache(maxsize=100, ttl=5 * 60))
|
|
41
44
|
def get_single_token_to_usd_rate(token_address: ChecksumAddress) -> USD:
|
|
42
|
-
# (w)xDai
|
|
43
|
-
if WRAPPED_XDAI_CONTRACT_ADDRESS
|
|
45
|
+
# (w)xDai and USDC are stablecoins pegged to USD, so use it to estimate USD worth.
|
|
46
|
+
if token_address in [WRAPPED_XDAI_CONTRACT_ADDRESS, USDCeContract().address]:
|
|
44
47
|
return USD(1.0)
|
|
45
48
|
# sDai is ERC4626 with wxDai as asset, we can take the rate directly from there instead of calling CoW.
|
|
46
49
|
if SDAI_CONTRACT_ADDRESS == token_address:
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
import os
|
|
2
3
|
import subprocess
|
|
4
|
+
import time
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from math import prod
|
|
5
|
-
from typing import Any, NoReturn, Optional, Type, TypeVar
|
|
7
|
+
from typing import Any, Callable, NoReturn, Optional, Type, TypeVar, cast
|
|
6
8
|
|
|
9
|
+
import httpx
|
|
7
10
|
import pytz
|
|
8
11
|
import requests
|
|
9
12
|
from pydantic import BaseModel, ValidationError
|
|
10
13
|
from scipy.optimize import newton
|
|
11
14
|
from scipy.stats import entropy
|
|
15
|
+
from tenacity import RetryError
|
|
12
16
|
|
|
13
17
|
from prediction_market_agent_tooling.gtypes import (
|
|
14
18
|
CollateralToken,
|
|
@@ -54,6 +58,49 @@ def check_not_none(
|
|
|
54
58
|
return value
|
|
55
59
|
|
|
56
60
|
|
|
61
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def retry_until_true(
|
|
65
|
+
condition: Callable[[Any], bool],
|
|
66
|
+
max_retries: int = 3,
|
|
67
|
+
delay: float = 1.0,
|
|
68
|
+
) -> Callable[[F], F]:
|
|
69
|
+
"""
|
|
70
|
+
Decorator that retries a function if the condition on its result evaluates to False.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
condition: Function that takes the result and returns True if acceptable, False to retry
|
|
74
|
+
max_retries: Maximum number of retry attempts
|
|
75
|
+
delay: Delay between retries in seconds
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def decorator(func: F) -> F:
|
|
79
|
+
@functools.wraps(func)
|
|
80
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
81
|
+
for attempt in range(1, max_retries + 1):
|
|
82
|
+
result = func(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
if condition(result):
|
|
85
|
+
logger.info(f"Condition met for {func.__name__} on {attempt}.")
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
if attempt < max_retries:
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"Retry {attempt + 1}/{max_retries} for {func.__name__}"
|
|
91
|
+
)
|
|
92
|
+
time.sleep(delay)
|
|
93
|
+
else:
|
|
94
|
+
logger.warning(
|
|
95
|
+
f"All {max_retries} retries exhausted for {func.__name__}"
|
|
96
|
+
)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
return cast(F, wrapper)
|
|
100
|
+
|
|
101
|
+
return decorator
|
|
102
|
+
|
|
103
|
+
|
|
57
104
|
def should_not_happen(
|
|
58
105
|
msg: str = "Should not happen.", exp: Type[ValueError] = ValueError
|
|
59
106
|
) -> NoReturn:
|
|
@@ -142,7 +189,9 @@ def get_current_git_url() -> str:
|
|
|
142
189
|
return git.Repo(search_parent_directories=True).remotes.origin.url
|
|
143
190
|
|
|
144
191
|
|
|
145
|
-
def response_to_json(
|
|
192
|
+
def response_to_json(
|
|
193
|
+
response: requests.models.Response | httpx.Response,
|
|
194
|
+
) -> dict[str, Any]:
|
|
146
195
|
response.raise_for_status()
|
|
147
196
|
response_json: dict[str, Any] = response.json()
|
|
148
197
|
return response_json
|
|
@@ -152,7 +201,7 @@ BaseModelT = TypeVar("BaseModelT", bound=BaseModel)
|
|
|
152
201
|
|
|
153
202
|
|
|
154
203
|
def response_to_model(
|
|
155
|
-
response: requests.models.Response, model: Type[BaseModelT]
|
|
204
|
+
response: requests.models.Response | httpx.Response, model: Type[BaseModelT]
|
|
156
205
|
) -> BaseModelT:
|
|
157
206
|
response_json = response_to_json(response)
|
|
158
207
|
try:
|
|
@@ -230,3 +279,12 @@ def calculate_sell_amount_in_collateral(
|
|
|
230
279
|
|
|
231
280
|
amount_to_sell = newton(f, 0)
|
|
232
281
|
return CollateralToken(float(amount_to_sell) * 0.999999) # Avoid rounding errors
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def extract_error_from_retry_error(e: BaseException | RetryError) -> BaseException:
|
|
285
|
+
if (
|
|
286
|
+
isinstance(e, RetryError)
|
|
287
|
+
and (exp_from_retry := e.last_attempt.exception()) is not None
|
|
288
|
+
):
|
|
289
|
+
e = exp_from_retry
|
|
290
|
+
return e
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import binascii
|
|
2
|
+
import multiprocessing.context
|
|
2
3
|
import secrets
|
|
3
4
|
from typing import Any, Optional
|
|
4
5
|
|
|
@@ -7,12 +8,22 @@ import tenacity
|
|
|
7
8
|
from eth_account import Account
|
|
8
9
|
from eth_typing import URI
|
|
9
10
|
from eth_utils.currency import MAX_WEI, MIN_WEI
|
|
11
|
+
from joblib import Parallel, delayed
|
|
10
12
|
from pydantic.types import SecretStr
|
|
11
13
|
from safe_eth.eth import EthereumClient
|
|
14
|
+
from safe_eth.eth.ethereum_client import TxSpeed
|
|
12
15
|
from safe_eth.safe.safe import SafeV141
|
|
13
16
|
from web3 import Web3
|
|
14
17
|
from web3.constants import HASH_ZERO
|
|
15
|
-
from web3.
|
|
18
|
+
from web3.contract.contract import ContractFunction as Web3ContractFunction
|
|
19
|
+
from web3.types import (
|
|
20
|
+
AccessList,
|
|
21
|
+
AccessListEntry,
|
|
22
|
+
BlockIdentifier,
|
|
23
|
+
Nonce,
|
|
24
|
+
TxParams,
|
|
25
|
+
TxReceipt,
|
|
26
|
+
)
|
|
16
27
|
|
|
17
28
|
from prediction_market_agent_tooling.gtypes import (
|
|
18
29
|
ABI,
|
|
@@ -67,17 +78,23 @@ def unwrap_generic_value(value: Any) -> Any:
|
|
|
67
78
|
return value.value
|
|
68
79
|
elif isinstance(value, list):
|
|
69
80
|
return [unwrap_generic_value(v) for v in value]
|
|
81
|
+
elif isinstance(value, tuple):
|
|
82
|
+
return tuple(unwrap_generic_value(v) for v in value)
|
|
70
83
|
elif isinstance(value, dict):
|
|
71
84
|
return {k: unwrap_generic_value(v) for k, v in value.items()}
|
|
72
85
|
return value
|
|
73
86
|
|
|
74
87
|
|
|
75
|
-
def parse_function_params(
|
|
88
|
+
def parse_function_params(
|
|
89
|
+
params: Optional[list[Any] | tuple[Any] | dict[str, Any]],
|
|
90
|
+
) -> list[Any] | tuple[Any]:
|
|
76
91
|
params = unwrap_generic_value(params)
|
|
77
92
|
if params is None:
|
|
78
93
|
return []
|
|
79
94
|
if isinstance(params, list):
|
|
80
|
-
return params
|
|
95
|
+
return [unwrap_generic_value(i) for i in params]
|
|
96
|
+
if isinstance(params, tuple):
|
|
97
|
+
return tuple(unwrap_generic_value(i) for i in params)
|
|
81
98
|
if isinstance(params, dict):
|
|
82
99
|
return list(params.values())
|
|
83
100
|
raise ValueError(f"Invalid type for function parameters: {type(params)}")
|
|
@@ -96,9 +113,12 @@ def call_function_on_contract(
|
|
|
96
113
|
contract_abi: ABI,
|
|
97
114
|
function_name: str,
|
|
98
115
|
function_params: Optional[list[Any] | dict[str, Any]] = None,
|
|
116
|
+
block_identifier: Optional[BlockIdentifier] = None,
|
|
99
117
|
) -> Any:
|
|
100
118
|
contract = web3.eth.contract(address=contract_address, abi=contract_abi)
|
|
101
|
-
output = contract.functions[function_name](
|
|
119
|
+
output = contract.functions[function_name](
|
|
120
|
+
*parse_function_params(function_params)
|
|
121
|
+
).call(block_identifier=block_identifier)
|
|
102
122
|
return output
|
|
103
123
|
|
|
104
124
|
|
|
@@ -111,14 +131,43 @@ def prepare_tx(
|
|
|
111
131
|
function_params: Optional[list[Any] | dict[str, Any]] = None,
|
|
112
132
|
access_list: Optional[AccessList] = None,
|
|
113
133
|
tx_params: Optional[TxParams] = None,
|
|
134
|
+
default_gas: int | None = None,
|
|
114
135
|
) -> TxParams:
|
|
115
136
|
tx_params_new = _prepare_tx_params(web3, from_address, access_list, tx_params)
|
|
116
137
|
contract = web3.eth.contract(address=contract_address, abi=contract_abi)
|
|
117
|
-
|
|
118
138
|
# Build the transaction.
|
|
119
|
-
function_call = contract.functions[function_name](
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
function_call = contract.functions[function_name](
|
|
140
|
+
*parse_function_params(function_params)
|
|
141
|
+
)
|
|
142
|
+
if default_gas is not None and "gas" not in tx_params_new:
|
|
143
|
+
tx_params_new["gas"] = estimate_gas_with_timeout(
|
|
144
|
+
function_call, default_gas=default_gas, tx_params=tx_params_new
|
|
145
|
+
)
|
|
146
|
+
built_tx_params: TxParams = function_call.build_transaction(tx_params_new)
|
|
147
|
+
return built_tx_params
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def estimate_gas_with_timeout(
|
|
151
|
+
function_call: Web3ContractFunction,
|
|
152
|
+
default_gas: int,
|
|
153
|
+
timeout: int = 15,
|
|
154
|
+
tx_params: Optional[TxParams] = None,
|
|
155
|
+
) -> int:
|
|
156
|
+
"""
|
|
157
|
+
Tries to estimate the gas, but default to the default value on timeout.
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
# We need n_jobs=-1, otherwise timeouting isn't applied.
|
|
161
|
+
result: list[int] = Parallel(n_jobs=-1, backend="threading", timeout=timeout)(
|
|
162
|
+
delayed(function_call.estimate_gas)(tx_params) for _ in range(1)
|
|
163
|
+
)
|
|
164
|
+
estimated_gas = result[0]
|
|
165
|
+
return int(estimated_gas * 1.2) # Add 20% buffer
|
|
166
|
+
except (TimeoutError, multiprocessing.context.TimeoutError):
|
|
167
|
+
logger.warning(
|
|
168
|
+
f"Gas estimation timed out after {timeout} seconds, using default: {default_gas}"
|
|
169
|
+
)
|
|
170
|
+
return default_gas
|
|
122
171
|
|
|
123
172
|
|
|
124
173
|
def _prepare_tx_params(
|
|
@@ -172,6 +221,7 @@ def send_function_on_contract_tx(
|
|
|
172
221
|
function_params: Optional[list[Any] | dict[str, Any]] = None,
|
|
173
222
|
tx_params: Optional[TxParams] = None,
|
|
174
223
|
timeout: int = 180,
|
|
224
|
+
default_gas: int | None = None,
|
|
175
225
|
) -> TxReceipt:
|
|
176
226
|
public_key = private_key_to_public_key(from_private_key)
|
|
177
227
|
|
|
@@ -183,6 +233,7 @@ def send_function_on_contract_tx(
|
|
|
183
233
|
function_name=function_name,
|
|
184
234
|
function_params=function_params,
|
|
185
235
|
tx_params=tx_params,
|
|
236
|
+
default_gas=default_gas,
|
|
186
237
|
)
|
|
187
238
|
|
|
188
239
|
receipt_tx = sign_send_and_get_receipt_tx(
|
|
@@ -211,6 +262,7 @@ def send_function_on_contract_tx_using_safe(
|
|
|
211
262
|
function_params: Optional[list[Any] | dict[str, Any]] = None,
|
|
212
263
|
tx_params: Optional[TxParams] = None,
|
|
213
264
|
timeout: int = 180,
|
|
265
|
+
default_gas: int | None = None,
|
|
214
266
|
) -> TxReceipt:
|
|
215
267
|
if not web3.provider.endpoint_uri: # type: ignore
|
|
216
268
|
raise EnvironmentError("RPC_URL not available in web3 object.")
|
|
@@ -251,6 +303,7 @@ def send_function_on_contract_tx_using_safe(
|
|
|
251
303
|
function_params=function_params,
|
|
252
304
|
access_list=access_list,
|
|
253
305
|
tx_params=tx_params,
|
|
306
|
+
default_gas=default_gas,
|
|
254
307
|
)
|
|
255
308
|
safe_tx = s.build_multisig_tx(
|
|
256
309
|
to=Web3.to_checksum_address(tx_params["to"]),
|
|
@@ -263,6 +316,7 @@ def send_function_on_contract_tx_using_safe(
|
|
|
263
316
|
tx_hash, tx = safe_tx.execute(
|
|
264
317
|
from_private_key.get_secret_value(),
|
|
265
318
|
tx_nonce=eoa_nonce,
|
|
319
|
+
eip1559_speed=TxSpeed.FAST,
|
|
266
320
|
)
|
|
267
321
|
receipt_tx = web3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
|
|
268
322
|
check_tx_receipt(receipt_tx)
|
|
@@ -280,7 +334,7 @@ def sign_send_and_get_receipt_tx(
|
|
|
280
334
|
tx_params_new, private_key=from_private_key.get_secret_value()
|
|
281
335
|
)
|
|
282
336
|
# Send the signed transaction.
|
|
283
|
-
send_tx = web3.eth.send_raw_transaction(signed_tx.
|
|
337
|
+
send_tx = web3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
284
338
|
# And wait for the receipt.
|
|
285
339
|
receipt_tx = web3.eth.wait_for_transaction_receipt(send_tx, timeout=timeout)
|
|
286
340
|
# Verify it didn't fail.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: prediction-market-agent-tooling
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.69.17.dev1149
|
|
4
4
|
Summary: Tools to benchmark, deploy and monitor prediction market agents.
|
|
5
|
+
License-File: LICENSE
|
|
5
6
|
Author: Gnosis
|
|
6
7
|
Requires-Python: >=3.10,<3.13
|
|
7
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -14,11 +15,11 @@ Provides-Extra: openai
|
|
|
14
15
|
Provides-Extra: optuna
|
|
15
16
|
Requires-Dist: autoflake (>=2.2.1,<3.0.0)
|
|
16
17
|
Requires-Dist: base58 (>=1.0.2,<2.0)
|
|
17
|
-
Requires-Dist: cowdao-cowpy
|
|
18
|
+
Requires-Dist: cowdao-cowpy (==1.0.1)
|
|
18
19
|
Requires-Dist: cron-validator (>=1.0.8,<2.0.0)
|
|
19
|
-
Requires-Dist: eth-account (>=0.
|
|
20
|
+
Requires-Dist: eth-account (>=0.13.0,<0.14.0)
|
|
20
21
|
Requires-Dist: eth-keys (>=0.6.1,<0.7.0)
|
|
21
|
-
Requires-Dist: eth-typing (>=
|
|
22
|
+
Requires-Dist: eth-typing (>=5.0.0,<6.0.0)
|
|
22
23
|
Requires-Dist: functions-framework (>=3.5.0,<4.0.0)
|
|
23
24
|
Requires-Dist: google-api-python-client (==2.95.0) ; extra == "google"
|
|
24
25
|
Requires-Dist: google-cloud-functions (>=1.16.0,<2.0.0)
|
|
@@ -41,14 +42,16 @@ Requires-Dist: prompt-toolkit (>=3.0.43,<4.0.0)
|
|
|
41
42
|
Requires-Dist: proto-plus (>=1.0.0,<2.0.0)
|
|
42
43
|
Requires-Dist: protobuf (>=5.0.0,<6.0.0)
|
|
43
44
|
Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0)
|
|
45
|
+
Requires-Dist: py-clob-client (>=0.24.0,<0.25.0)
|
|
44
46
|
Requires-Dist: pydantic (>=2.6.1,<3.0.0)
|
|
47
|
+
Requires-Dist: pydantic-ai (>=1.0.3,<2.0.0)
|
|
45
48
|
Requires-Dist: pydantic-settings (>=2.4.0,<3.0.0)
|
|
46
49
|
Requires-Dist: pymongo (>=4.8.0,<5.0.0)
|
|
47
50
|
Requires-Dist: pytest-postgresql (>=6.1.1,<7.0.0)
|
|
48
51
|
Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
|
|
49
52
|
Requires-Dist: python-json-logger (>=3.3.0,<4.0.0)
|
|
50
|
-
Requires-Dist: safe-cli (>=1.
|
|
51
|
-
Requires-Dist: safe-eth-py (>=
|
|
53
|
+
Requires-Dist: safe-cli (>=1.5.0,<2.0.0)
|
|
54
|
+
Requires-Dist: safe-eth-py (>=7.8.0,<8.0.0)
|
|
52
55
|
Requires-Dist: scikit-learn (>=1.3.1,<2.0.0)
|
|
53
56
|
Requires-Dist: sqlmodel (>=0.0.22,<0.0.23)
|
|
54
57
|
Requires-Dist: streamlit (>=1.31.0,<2.0.0)
|
|
@@ -61,7 +64,7 @@ Requires-Dist: types-cachetools (>=5.5.0.20240820,<6.0.0.0)
|
|
|
61
64
|
Requires-Dist: types-python-dateutil (>=2.9.0.20240906,<3.0.0.0)
|
|
62
65
|
Requires-Dist: types-pytz (>=2024.1.0.20240203,<2025.0.0.0)
|
|
63
66
|
Requires-Dist: types-requests (>=2.31.0.0,<3.0.0.0)
|
|
64
|
-
Requires-Dist: web3 (>=6
|
|
67
|
+
Requires-Dist: web3 (>=6,<8)
|
|
65
68
|
Description-Content-Type: text/markdown
|
|
66
69
|
|
|
67
70
|
# Prediction Market Agent Tooling
|
|
@@ -99,7 +102,8 @@ For example:
|
|
|
99
102
|
```python
|
|
100
103
|
import prediction_market_agent_tooling.benchmark.benchmark as bm
|
|
101
104
|
from prediction_market_agent_tooling.benchmark.agents import RandomAgent
|
|
102
|
-
from prediction_market_agent_tooling.markets.
|
|
105
|
+
from prediction_market_agent_tooling.markets.market_type import MarketType
|
|
106
|
+
from prediction_market_agent_tooling.markets.markets import get_binary_markets
|
|
103
107
|
|
|
104
108
|
benchmarker = bm.Benchmarker(
|
|
105
109
|
markets=get_binary_markets(limit=10, market_type=MarketType.MANIFOLD),
|