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,28 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from prediction_market_agent_tooling.gtypes import ChainID, ChecksumAddress
|
|
4
|
+
from prediction_market_agent_tooling.markets.seer.data_models import SeerTransaction
|
|
5
|
+
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
|
6
|
+
from prediction_market_agent_tooling.tools.utils import to_int_timestamp, utcnow
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_seer_transactions(
|
|
10
|
+
account: ChecksumAddress,
|
|
11
|
+
chain_id: ChainID,
|
|
12
|
+
start_time: DatetimeUTC | None = None,
|
|
13
|
+
end_time: DatetimeUTC | None = None,
|
|
14
|
+
timeout: int = 60, # The endpoint is pretty slow to respond atm.
|
|
15
|
+
) -> list[SeerTransaction]:
|
|
16
|
+
url = "https://app.seer.pm/.netlify/functions/get-transactions"
|
|
17
|
+
params: dict[str, str | int] = {
|
|
18
|
+
"account": account,
|
|
19
|
+
"chainId": chain_id,
|
|
20
|
+
"startTime": to_int_timestamp(start_time) if start_time else 0,
|
|
21
|
+
"endTime": to_int_timestamp(end_time if end_time else utcnow()),
|
|
22
|
+
}
|
|
23
|
+
response = httpx.get(url, params=params, timeout=timeout)
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
response_json = response.json()
|
|
26
|
+
|
|
27
|
+
transactions = [SeerTransaction.model_validate(tx) for tx in response_json]
|
|
28
|
+
return transactions
|
|
@@ -8,14 +8,20 @@ from prediction_market_agent_tooling.gtypes import (
|
|
|
8
8
|
ABI,
|
|
9
9
|
ChecksumAddress,
|
|
10
10
|
OutcomeStr,
|
|
11
|
+
OutcomeWei,
|
|
11
12
|
TxReceipt,
|
|
13
|
+
Wei,
|
|
12
14
|
xDai,
|
|
13
15
|
)
|
|
14
|
-
from prediction_market_agent_tooling.markets.seer.data_models import
|
|
16
|
+
from prediction_market_agent_tooling.markets.seer.data_models import (
|
|
17
|
+
ExactInputSingleParams,
|
|
18
|
+
QuoteExactInputSingleParams,
|
|
19
|
+
)
|
|
15
20
|
from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
|
|
16
21
|
CreateCategoricalMarketsParams,
|
|
17
22
|
)
|
|
18
23
|
from prediction_market_agent_tooling.tools.contract import (
|
|
24
|
+
ContractERC20OnGnosisChain,
|
|
19
25
|
ContractOnGnosisChain,
|
|
20
26
|
abi_field_validator,
|
|
21
27
|
)
|
|
@@ -83,7 +89,7 @@ class SeerMarketFactory(ContractOnGnosisChain):
|
|
|
83
89
|
|
|
84
90
|
|
|
85
91
|
class GnosisRouter(ContractOnGnosisChain):
|
|
86
|
-
# https://gnosisscan.io/address/
|
|
92
|
+
# https://gnosisscan.io/address/0xeC9048b59b3467415b1a38F63416407eA0c70fB8#code.
|
|
87
93
|
abi: ABI = abi_field_validator(
|
|
88
94
|
os.path.join(
|
|
89
95
|
os.path.dirname(os.path.realpath(__file__)),
|
|
@@ -97,12 +103,18 @@ class GnosisRouter(ContractOnGnosisChain):
|
|
|
97
103
|
def redeem_to_base(
|
|
98
104
|
self,
|
|
99
105
|
api_keys: APIKeys,
|
|
100
|
-
|
|
106
|
+
market: ChecksumAddress,
|
|
107
|
+
outcome_indexes: list[int],
|
|
108
|
+
amounts: list[OutcomeWei],
|
|
101
109
|
web3: Web3 | None = None,
|
|
102
110
|
) -> TxReceipt:
|
|
103
|
-
params_dict = params.model_dump(by_alias=True)
|
|
104
111
|
# We explicity set amounts since OutcomeWei gets serialized as dict
|
|
105
|
-
params_dict
|
|
112
|
+
params_dict = {
|
|
113
|
+
"market": market,
|
|
114
|
+
"outcomeIndexes": outcome_indexes,
|
|
115
|
+
"amounts": [amount.value for amount in amounts],
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
receipt_tx = self.send(
|
|
107
119
|
api_keys=api_keys,
|
|
108
120
|
function_name="redeemToBase",
|
|
@@ -110,3 +122,101 @@ class GnosisRouter(ContractOnGnosisChain):
|
|
|
110
122
|
web3=web3,
|
|
111
123
|
)
|
|
112
124
|
return receipt_tx
|
|
125
|
+
|
|
126
|
+
def split_from_base(
|
|
127
|
+
self,
|
|
128
|
+
api_keys: APIKeys,
|
|
129
|
+
market_id: ChecksumAddress,
|
|
130
|
+
amount_wei: Wei,
|
|
131
|
+
web3: Web3 | None = None,
|
|
132
|
+
) -> TxReceipt:
|
|
133
|
+
"""Splits using xDAI and receives outcome tokens"""
|
|
134
|
+
return self.send_with_value(
|
|
135
|
+
api_keys=api_keys,
|
|
136
|
+
function_name="splitFromBase",
|
|
137
|
+
amount_wei=amount_wei,
|
|
138
|
+
function_params=[market_id],
|
|
139
|
+
web3=web3,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def split_position(
|
|
143
|
+
self,
|
|
144
|
+
api_keys: APIKeys,
|
|
145
|
+
collateral_token: ChecksumAddress,
|
|
146
|
+
market_id: ChecksumAddress,
|
|
147
|
+
amount: Wei,
|
|
148
|
+
web3: Web3 | None = None,
|
|
149
|
+
) -> TxReceipt:
|
|
150
|
+
"""Splits collateral token into full set of outcome tokens."""
|
|
151
|
+
receipt_tx = self.send(
|
|
152
|
+
api_keys=api_keys,
|
|
153
|
+
function_name="splitPosition",
|
|
154
|
+
function_params=[collateral_token, market_id, amount],
|
|
155
|
+
web3=web3,
|
|
156
|
+
)
|
|
157
|
+
return receipt_tx
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SwaprRouterContract(ContractOnGnosisChain):
|
|
161
|
+
abi: ABI = abi_field_validator(
|
|
162
|
+
os.path.join(
|
|
163
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
164
|
+
"../../abis/swapr_router.abi.json",
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
address: ChecksumAddress = Web3.to_checksum_address(
|
|
169
|
+
"0xffb643e73f280b97809a8b41f7232ab401a04ee1"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def exact_input_single(
|
|
173
|
+
self,
|
|
174
|
+
api_keys: APIKeys,
|
|
175
|
+
params: ExactInputSingleParams,
|
|
176
|
+
web3: Web3 | None = None,
|
|
177
|
+
) -> TxReceipt:
|
|
178
|
+
erc20_token = ContractERC20OnGnosisChain(address=params.token_in)
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
erc20_token.allowance(api_keys.bet_from_address, self.address, web3=web3)
|
|
182
|
+
< params.amount_in
|
|
183
|
+
):
|
|
184
|
+
erc20_token.approve(api_keys, self.address, params.amount_in, web3=web3)
|
|
185
|
+
|
|
186
|
+
return self.send(
|
|
187
|
+
api_keys=api_keys,
|
|
188
|
+
function_name="exactInputSingle",
|
|
189
|
+
function_params=[tuple(dict(params).values())],
|
|
190
|
+
web3=web3,
|
|
191
|
+
# Use higher gas limit for complex swap operations to avoid slow estimation
|
|
192
|
+
# Typical Swapr swaps use 150k-300k gas, we set conservative
|
|
193
|
+
default_gas=400_000,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class SwaprQuoterContract(ContractOnGnosisChain):
|
|
198
|
+
# File content taken from https://gnosisscan.io/address/0xcBaD9FDf0D2814659Eb26f600EFDeAF005Eda0F7#code.
|
|
199
|
+
abi: ABI = abi_field_validator(
|
|
200
|
+
os.path.join(
|
|
201
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
202
|
+
"../../abis/swapr_quoter.abi.json",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
address: ChecksumAddress = Web3.to_checksum_address(
|
|
207
|
+
"0xcBaD9FDf0D2814659Eb26f600EFDeAF005Eda0F7"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def quote_exact_input_single(
|
|
211
|
+
self,
|
|
212
|
+
params: QuoteExactInputSingleParams,
|
|
213
|
+
web3: Web3 | None = None,
|
|
214
|
+
) -> tuple[Wei, Wei]:
|
|
215
|
+
# See https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps.
|
|
216
|
+
result: tuple[int, int] = self.call(
|
|
217
|
+
function_name="quoteExactInputSingle",
|
|
218
|
+
function_params=list(dict(params).values()),
|
|
219
|
+
web3=web3,
|
|
220
|
+
)
|
|
221
|
+
amount_out, fee = result
|
|
222
|
+
return Wei(amount_out), Wei(fee)
|
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import typing as t
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from enum import Enum
|
|
3
5
|
from typing import Any
|
|
4
6
|
|
|
5
7
|
from subgrounds import FieldPath
|
|
6
8
|
from web3.constants import ADDRESS_ZERO
|
|
7
9
|
|
|
8
10
|
from prediction_market_agent_tooling.deploy.constants import (
|
|
11
|
+
DOWN_OUTCOME_LOWERCASE_IDENTIFIER,
|
|
9
12
|
NO_OUTCOME_LOWERCASE_IDENTIFIER,
|
|
13
|
+
UP_OUTCOME_LOWERCASE_IDENTIFIER,
|
|
10
14
|
YES_OUTCOME_LOWERCASE_IDENTIFIER,
|
|
11
15
|
)
|
|
12
16
|
from prediction_market_agent_tooling.gtypes import ChecksumAddress, Wei
|
|
13
17
|
from prediction_market_agent_tooling.loggers import logger
|
|
14
|
-
from prediction_market_agent_tooling.markets.agent_market import
|
|
18
|
+
from prediction_market_agent_tooling.markets.agent_market import (
|
|
19
|
+
ConditionalFilterType,
|
|
20
|
+
FilterBy,
|
|
21
|
+
QuestionType,
|
|
22
|
+
SortBy,
|
|
23
|
+
)
|
|
15
24
|
from prediction_market_agent_tooling.markets.base_subgraph_handler import (
|
|
16
25
|
BaseSubgraphHandler,
|
|
17
26
|
)
|
|
18
|
-
from prediction_market_agent_tooling.markets.seer.data_models import
|
|
19
|
-
|
|
27
|
+
from prediction_market_agent_tooling.markets.seer.data_models import (
|
|
28
|
+
SeerMarket,
|
|
29
|
+
SeerMarketQuestions,
|
|
30
|
+
SeerMarketWithQuestions,
|
|
31
|
+
)
|
|
32
|
+
from prediction_market_agent_tooling.markets.seer.subgraph_data_models import (
|
|
33
|
+
SwaprPool,
|
|
34
|
+
SwaprSwap,
|
|
35
|
+
)
|
|
20
36
|
from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
|
|
21
|
-
from prediction_market_agent_tooling.tools.
|
|
37
|
+
from prediction_market_agent_tooling.tools.singleton import SingletonMeta
|
|
38
|
+
from prediction_market_agent_tooling.tools.utils import (
|
|
39
|
+
DatetimeUTC,
|
|
40
|
+
to_int_timestamp,
|
|
41
|
+
utcnow,
|
|
42
|
+
)
|
|
22
43
|
from prediction_market_agent_tooling.tools.web3_utils import unwrap_generic_value
|
|
23
44
|
|
|
24
45
|
|
|
46
|
+
class TemplateId(int, Enum):
|
|
47
|
+
"""Template IDs used in Reality.eth questions."""
|
|
48
|
+
|
|
49
|
+
SCALAR = 1
|
|
50
|
+
CATEGORICAL = 2
|
|
51
|
+
MULTICATEGORICAL = 3
|
|
52
|
+
|
|
53
|
+
|
|
25
54
|
class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
26
55
|
"""
|
|
27
56
|
Class responsible for handling interactions with Seer subgraphs.
|
|
@@ -47,7 +76,9 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
|
47
76
|
)
|
|
48
77
|
)
|
|
49
78
|
|
|
50
|
-
def _get_fields_for_markets(
|
|
79
|
+
def _get_fields_for_markets(
|
|
80
|
+
self, markets_field: FieldPath, current_level: int = 0, max_level: int = 1
|
|
81
|
+
) -> list[FieldPath]:
|
|
51
82
|
fields = [
|
|
52
83
|
markets_field.id,
|
|
53
84
|
markets_field.factory,
|
|
@@ -61,12 +92,26 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
|
61
92
|
markets_field.payoutNumerators,
|
|
62
93
|
markets_field.hasAnswers,
|
|
63
94
|
markets_field.blockTimestamp,
|
|
64
|
-
markets_field.parentMarket.id,
|
|
65
95
|
markets_field.openingTs,
|
|
66
96
|
markets_field.finalizeTs,
|
|
67
97
|
markets_field.wrappedTokens,
|
|
68
98
|
markets_field.collateralToken,
|
|
99
|
+
markets_field.upperBound,
|
|
100
|
+
markets_field.lowerBound,
|
|
101
|
+
markets_field.templateId,
|
|
69
102
|
]
|
|
103
|
+
if current_level < max_level:
|
|
104
|
+
fields.extend(
|
|
105
|
+
self._get_fields_for_markets(
|
|
106
|
+
markets_field.parentMarket, current_level + 1, max_level
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
# TODO: Same situation as with `questions` field above.
|
|
110
|
+
# fields.extend(
|
|
111
|
+
# self._get_fields_for_markets(
|
|
112
|
+
# markets_field.childMarkets, current_level + 1, max_level
|
|
113
|
+
# )
|
|
114
|
+
# )
|
|
70
115
|
return fields
|
|
71
116
|
|
|
72
117
|
@staticmethod
|
|
@@ -74,12 +119,30 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
|
74
119
|
# We do an extra check for the invalid outcome for safety.
|
|
75
120
|
return [m for m in markets if len(m.outcomes) == 3]
|
|
76
121
|
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _create_case_variations_condition(
|
|
124
|
+
identifier: str,
|
|
125
|
+
outcome_condition: str = "outcomes_contains",
|
|
126
|
+
condition: str = "or",
|
|
127
|
+
) -> dict[str, list[dict[str, list[str]]]]:
|
|
128
|
+
return {
|
|
129
|
+
condition: [
|
|
130
|
+
{outcome_condition: [variation]}
|
|
131
|
+
for variation in [
|
|
132
|
+
identifier.lower(),
|
|
133
|
+
identifier.capitalize(),
|
|
134
|
+
identifier.upper(),
|
|
135
|
+
]
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
77
139
|
@staticmethod
|
|
78
140
|
def _build_where_statements(
|
|
79
141
|
filter_by: FilterBy,
|
|
80
142
|
outcome_supply_gt_if_open: Wei,
|
|
81
|
-
|
|
82
|
-
|
|
143
|
+
question_type: QuestionType = QuestionType.ALL,
|
|
144
|
+
conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
|
|
145
|
+
parent_market_id: HexBytes | None = None,
|
|
83
146
|
) -> dict[Any, Any]:
|
|
84
147
|
now = to_int_timestamp(utcnow())
|
|
85
148
|
|
|
@@ -98,31 +161,62 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
|
98
161
|
case _:
|
|
99
162
|
raise ValueError(f"Unknown filter {filter_by}")
|
|
100
163
|
|
|
101
|
-
if
|
|
102
|
-
and_stms["parentMarket"] =
|
|
164
|
+
if parent_market_id:
|
|
165
|
+
and_stms["parentMarket"] = parent_market_id.to_0x_hex().lower()
|
|
103
166
|
|
|
104
|
-
|
|
105
|
-
yes_stms, no_stms = {}, {}
|
|
106
|
-
if not include_categorical_markets:
|
|
107
|
-
# Create single OR conditions with all variations
|
|
108
|
-
yes_stms["or"] = [
|
|
109
|
-
{"outcomes_contains": [variation]}
|
|
110
|
-
for variation in [
|
|
111
|
-
YES_OUTCOME_LOWERCASE_IDENTIFIER,
|
|
112
|
-
YES_OUTCOME_LOWERCASE_IDENTIFIER.capitalize(),
|
|
113
|
-
YES_OUTCOME_LOWERCASE_IDENTIFIER.upper(),
|
|
114
|
-
]
|
|
115
|
-
]
|
|
116
|
-
no_stms["or"] = [
|
|
117
|
-
{"outcomes_contains": [variation]}
|
|
118
|
-
for variation in [
|
|
119
|
-
NO_OUTCOME_LOWERCASE_IDENTIFIER,
|
|
120
|
-
NO_OUTCOME_LOWERCASE_IDENTIFIER.capitalize(),
|
|
121
|
-
NO_OUTCOME_LOWERCASE_IDENTIFIER.upper(),
|
|
122
|
-
]
|
|
123
|
-
]
|
|
167
|
+
outcome_filters: list[dict[str, t.Any]] = []
|
|
124
168
|
|
|
125
|
-
|
|
169
|
+
if question_type == QuestionType.SCALAR:
|
|
170
|
+
# Template ID "1" + UP/DOWN outcomes for scalar markets
|
|
171
|
+
and_stms["templateId"] = TemplateId.SCALAR.value
|
|
172
|
+
up_filter = SeerSubgraphHandler._create_case_variations_condition(
|
|
173
|
+
UP_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
|
|
174
|
+
)
|
|
175
|
+
down_filter = SeerSubgraphHandler._create_case_variations_condition(
|
|
176
|
+
DOWN_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
|
|
177
|
+
)
|
|
178
|
+
outcome_filters.extend([up_filter, down_filter])
|
|
179
|
+
|
|
180
|
+
elif question_type == QuestionType.BINARY:
|
|
181
|
+
# Template ID "2" + YES/NO outcomes for binary markets
|
|
182
|
+
and_stms["templateId"] = TemplateId.CATEGORICAL.value
|
|
183
|
+
yes_filter = SeerSubgraphHandler._create_case_variations_condition(
|
|
184
|
+
YES_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
|
|
185
|
+
)
|
|
186
|
+
no_filter = SeerSubgraphHandler._create_case_variations_condition(
|
|
187
|
+
NO_OUTCOME_LOWERCASE_IDENTIFIER, "outcomes_contains", "or"
|
|
188
|
+
)
|
|
189
|
+
outcome_filters.extend([yes_filter, no_filter])
|
|
190
|
+
|
|
191
|
+
elif question_type == QuestionType.CATEGORICAL:
|
|
192
|
+
# Template ID 2 (categorical) OR Template ID 3 (multi-categorical,
|
|
193
|
+
# we treat them as categorical for now for simplicity)
|
|
194
|
+
# https://reality.eth.limo/app/docs/html/contracts.html#templates
|
|
195
|
+
outcome_filters.append(
|
|
196
|
+
{
|
|
197
|
+
"or": [
|
|
198
|
+
{"templateId": TemplateId.CATEGORICAL.value},
|
|
199
|
+
{"templateId": TemplateId.MULTICATEGORICAL.value},
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Build filters for conditional_filter type
|
|
205
|
+
conditional_filter = {}
|
|
206
|
+
match conditional_filter_type:
|
|
207
|
+
case ConditionalFilterType.ONLY_CONDITIONAL:
|
|
208
|
+
conditional_filter["parentMarket_not"] = ADDRESS_ZERO.lower()
|
|
209
|
+
case ConditionalFilterType.ONLY_NOT_CONDITIONAL:
|
|
210
|
+
conditional_filter["parentMarket"] = ADDRESS_ZERO.lower()
|
|
211
|
+
case ConditionalFilterType.ALL:
|
|
212
|
+
pass
|
|
213
|
+
case _:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"Unknown conditional filter {conditional_filter_type}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
all_filters = outcome_filters + [and_stms, conditional_filter]
|
|
219
|
+
where_stms: dict[str, t.Any] = {"and": all_filters}
|
|
126
220
|
return where_stms
|
|
127
221
|
|
|
128
222
|
def _build_sort_params(
|
|
@@ -157,18 +251,18 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
|
157
251
|
sort_by: SortBy = SortBy.NONE,
|
|
158
252
|
limit: int | None = None,
|
|
159
253
|
outcome_supply_gt_if_open: Wei = Wei(0),
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
254
|
+
question_type: QuestionType = QuestionType.ALL,
|
|
255
|
+
conditional_filter_type: ConditionalFilterType = ConditionalFilterType.ONLY_NOT_CONDITIONAL,
|
|
256
|
+
parent_market_id: HexBytes | None = None,
|
|
257
|
+
) -> list[SeerMarketWithQuestions]:
|
|
163
258
|
sort_direction, sort_by_field = self._build_sort_params(sort_by)
|
|
164
259
|
|
|
165
|
-
"""Returns markets that contain 2 categories plus an invalid outcome."""
|
|
166
|
-
# Binary markets on Seer contain 3 outcomes: OutcomeA, outcomeB and an Invalid option.
|
|
167
260
|
where_stms = self._build_where_statements(
|
|
168
261
|
filter_by=filter_by,
|
|
169
262
|
outcome_supply_gt_if_open=outcome_supply_gt_if_open,
|
|
170
|
-
|
|
171
|
-
|
|
263
|
+
parent_market_id=parent_market_id,
|
|
264
|
+
question_type=question_type,
|
|
265
|
+
conditional_filter_type=conditional_filter_type,
|
|
172
266
|
)
|
|
173
267
|
|
|
174
268
|
# These values can not be set to `None`, but they can be omitted.
|
|
@@ -187,62 +281,123 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
|
187
281
|
)
|
|
188
282
|
fields = self._get_fields_for_markets(markets_field)
|
|
189
283
|
markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
|
|
190
|
-
|
|
284
|
+
market_ids = [m.id for m in markets]
|
|
285
|
+
# We fetch questions from all markets and all parents in one go
|
|
286
|
+
parent_market_ids = [
|
|
287
|
+
m.parent_market.id for m in markets if m.parent_market is not None
|
|
288
|
+
]
|
|
289
|
+
q = SeerQuestionsCache(seer_subgraph_handler=self)
|
|
290
|
+
q.fetch_questions(list(set(market_ids + parent_market_ids)))
|
|
291
|
+
|
|
292
|
+
# Create SeerMarketWithQuestions for each market
|
|
293
|
+
return [
|
|
294
|
+
SeerMarketWithQuestions(
|
|
295
|
+
**m.model_dump(), questions=q.market_id_to_questions[m.id]
|
|
296
|
+
)
|
|
297
|
+
for m in markets
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
def get_questions_for_markets(
|
|
301
|
+
self, market_ids: list[HexBytes]
|
|
302
|
+
) -> list[SeerMarketQuestions]:
|
|
303
|
+
where = unwrap_generic_value(
|
|
304
|
+
{"market_in": [market_id.to_0x_hex().lower() for market_id in market_ids]}
|
|
305
|
+
)
|
|
306
|
+
markets_field = self.seer_subgraph.Query.marketQuestions(where=where)
|
|
307
|
+
fields = self._get_fields_for_questions(markets_field)
|
|
308
|
+
questions = self.do_query(fields=fields, pydantic_model=SeerMarketQuestions)
|
|
309
|
+
return questions
|
|
191
310
|
|
|
192
|
-
def get_market_by_id(self, market_id: HexBytes) ->
|
|
193
|
-
markets_field = self.seer_subgraph.Query.market(
|
|
311
|
+
def get_market_by_id(self, market_id: HexBytes) -> SeerMarketWithQuestions:
|
|
312
|
+
markets_field = self.seer_subgraph.Query.market(
|
|
313
|
+
id=market_id.to_0x_hex().lower()
|
|
314
|
+
)
|
|
194
315
|
fields = self._get_fields_for_markets(markets_field)
|
|
195
316
|
markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
|
|
196
317
|
if len(markets) != 1:
|
|
197
318
|
raise ValueError(
|
|
198
319
|
f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
|
|
199
320
|
)
|
|
200
|
-
|
|
321
|
+
q = SeerQuestionsCache(self)
|
|
322
|
+
q.fetch_questions([market_id])
|
|
323
|
+
questions = q.market_id_to_questions[market_id]
|
|
324
|
+
s = SeerMarketWithQuestions.model_validate(
|
|
325
|
+
markets[0].model_dump() | {"questions": questions}
|
|
326
|
+
)
|
|
327
|
+
return s
|
|
201
328
|
|
|
202
|
-
def
|
|
329
|
+
def _get_fields_for_questions(self, questions_field: FieldPath) -> list[FieldPath]:
|
|
203
330
|
fields = [
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
pools_field.token1Price,
|
|
209
|
-
pools_field.token0.id,
|
|
210
|
-
pools_field.token0.name,
|
|
211
|
-
pools_field.token0.symbol,
|
|
212
|
-
pools_field.token1.id,
|
|
213
|
-
pools_field.token1.name,
|
|
214
|
-
pools_field.token1.symbol,
|
|
331
|
+
questions_field.question.id,
|
|
332
|
+
questions_field.question.best_answer,
|
|
333
|
+
questions_field.question.finalize_ts,
|
|
334
|
+
questions_field.market.id,
|
|
215
335
|
]
|
|
216
336
|
return fields
|
|
217
337
|
|
|
338
|
+
def get_market_by_wrapped_token(self, tokens: list[ChecksumAddress]) -> SeerMarket:
|
|
339
|
+
where_stms = {"wrappedTokens_contains": tokens}
|
|
340
|
+
markets_field = self.seer_subgraph.Query.markets(
|
|
341
|
+
where=unwrap_generic_value(where_stms)
|
|
342
|
+
)
|
|
343
|
+
fields = self._get_fields_for_markets(markets_field)
|
|
344
|
+
markets = self.do_query(fields=fields, pydantic_model=SeerMarket)
|
|
345
|
+
if len(markets) != 1:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"Fetched wrong number of markets. Expected 1 but got {len(markets)}"
|
|
348
|
+
)
|
|
349
|
+
return markets[0]
|
|
350
|
+
|
|
351
|
+
def _get_fields_for_seer_token(self, fields: FieldPath) -> list[FieldPath]:
|
|
352
|
+
return [
|
|
353
|
+
fields.id,
|
|
354
|
+
fields.name,
|
|
355
|
+
fields.symbol,
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]:
|
|
359
|
+
fields = (
|
|
360
|
+
[
|
|
361
|
+
pools_field.id,
|
|
362
|
+
pools_field.liquidity,
|
|
363
|
+
pools_field.sqrtPrice,
|
|
364
|
+
pools_field.token0Price,
|
|
365
|
+
pools_field.token1Price,
|
|
366
|
+
pools_field.totalValueLockedToken0,
|
|
367
|
+
pools_field.totalValueLockedToken1,
|
|
368
|
+
]
|
|
369
|
+
+ self._get_fields_for_seer_token(pools_field.token0)
|
|
370
|
+
+ self._get_fields_for_seer_token(pools_field.token1)
|
|
371
|
+
)
|
|
372
|
+
return fields
|
|
373
|
+
|
|
218
374
|
def get_pool_by_token(
|
|
219
375
|
self, token_address: ChecksumAddress, collateral_address: ChecksumAddress
|
|
220
|
-
) ->
|
|
376
|
+
) -> SwaprPool | None:
|
|
221
377
|
# We iterate through the wrapped tokens and put them in a where clause so that we hit the subgraph endpoint just once.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
[
|
|
378
|
+
|
|
379
|
+
where_argument = {
|
|
380
|
+
"or": [
|
|
225
381
|
{
|
|
226
|
-
"
|
|
227
|
-
"
|
|
382
|
+
"token0_": {"id": token_address.lower()},
|
|
383
|
+
"token1_": {"id": collateral_address.lower()},
|
|
228
384
|
},
|
|
229
385
|
{
|
|
230
|
-
"
|
|
231
|
-
"
|
|
386
|
+
"token0_": {"id": collateral_address.lower()},
|
|
387
|
+
"token1_": {"id": token_address.lower()},
|
|
232
388
|
},
|
|
233
389
|
]
|
|
234
|
-
|
|
235
|
-
|
|
390
|
+
}
|
|
236
391
|
optional_params = {}
|
|
237
392
|
optional_params["orderBy"] = self.swapr_algebra_subgraph.Pool.liquidity
|
|
238
393
|
optional_params["orderDirection"] = "desc"
|
|
239
394
|
|
|
240
395
|
pools_field = self.swapr_algebra_subgraph.Query.pools(
|
|
241
|
-
where=unwrap_generic_value(
|
|
396
|
+
where=unwrap_generic_value(where_argument), **optional_params
|
|
242
397
|
)
|
|
243
398
|
|
|
244
399
|
fields = self._get_fields_for_pools(pools_field)
|
|
245
|
-
pools = self.do_query(fields=fields, pydantic_model=
|
|
400
|
+
pools = self.do_query(fields=fields, pydantic_model=SwaprPool)
|
|
246
401
|
# We assume there is only one pool for outcomeToken/sDAI.
|
|
247
402
|
if len(pools) > 1:
|
|
248
403
|
logger.info(
|
|
@@ -252,3 +407,79 @@ class SeerSubgraphHandler(BaseSubgraphHandler):
|
|
|
252
407
|
# We select the first one
|
|
253
408
|
return pools[0]
|
|
254
409
|
return None
|
|
410
|
+
|
|
411
|
+
def _get_fields_for_swaps(self, swaps_field: FieldPath) -> list[FieldPath]:
|
|
412
|
+
fields = (
|
|
413
|
+
[
|
|
414
|
+
swaps_field.id,
|
|
415
|
+
swaps_field.pool.id,
|
|
416
|
+
swaps_field.sender,
|
|
417
|
+
swaps_field.recipient,
|
|
418
|
+
swaps_field.price,
|
|
419
|
+
swaps_field.amount0,
|
|
420
|
+
swaps_field.amount1,
|
|
421
|
+
swaps_field.timestamp,
|
|
422
|
+
]
|
|
423
|
+
+ self._get_fields_for_seer_token(swaps_field.token0)
|
|
424
|
+
+ self._get_fields_for_seer_token(swaps_field.token1)
|
|
425
|
+
)
|
|
426
|
+
return fields
|
|
427
|
+
|
|
428
|
+
def get_swaps(
|
|
429
|
+
self,
|
|
430
|
+
recipient: ChecksumAddress,
|
|
431
|
+
timestamp_gt: DatetimeUTC | None = None,
|
|
432
|
+
timestamp_lt: DatetimeUTC | None = None,
|
|
433
|
+
) -> list[SwaprSwap]:
|
|
434
|
+
where_argument: dict[str, Any] = {"recipient": recipient.lower()}
|
|
435
|
+
if timestamp_gt is not None:
|
|
436
|
+
where_argument["timestamp_gt"] = to_int_timestamp(timestamp_gt)
|
|
437
|
+
if timestamp_lt is not None:
|
|
438
|
+
where_argument["timestamp_lt"] = to_int_timestamp(timestamp_lt)
|
|
439
|
+
|
|
440
|
+
swaps_field = self.swapr_algebra_subgraph.Query.swaps(where=where_argument)
|
|
441
|
+
fields = self._get_fields_for_swaps(swaps_field)
|
|
442
|
+
swaps = self.do_query(fields=fields, pydantic_model=SwaprSwap)
|
|
443
|
+
|
|
444
|
+
return swaps
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class SeerQuestionsCache(metaclass=SingletonMeta):
|
|
448
|
+
"""A singleton cache for storing and retrieving Seer market questions.
|
|
449
|
+
|
|
450
|
+
This class provides an in-memory cache for Seer market questions, preventing
|
|
451
|
+
redundant subgraph queries by maintaining a mapping of market IDs to their
|
|
452
|
+
associated questions. It implements the singleton pattern to ensure a single
|
|
453
|
+
cache instance is used throughout the agent run.
|
|
454
|
+
|
|
455
|
+
Attributes:
|
|
456
|
+
market_id_to_questions: A dictionary mapping market IDs to lists of SeerMarketQuestions
|
|
457
|
+
seer_subgraph_handler: Handler for interacting with the Seer subgraph
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
def __init__(self, seer_subgraph_handler: SeerSubgraphHandler | None = None):
|
|
461
|
+
self.market_id_to_questions: dict[
|
|
462
|
+
HexBytes, list[SeerMarketQuestions]
|
|
463
|
+
] = defaultdict(list)
|
|
464
|
+
self.seer_subgraph_handler = seer_subgraph_handler or SeerSubgraphHandler()
|
|
465
|
+
|
|
466
|
+
def fetch_questions(self, market_ids: list[HexBytes]) -> None:
|
|
467
|
+
filtered_list = [
|
|
468
|
+
market_id
|
|
469
|
+
for market_id in market_ids
|
|
470
|
+
if market_id not in self.market_id_to_questions
|
|
471
|
+
]
|
|
472
|
+
if not filtered_list:
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
questions = self.seer_subgraph_handler.get_questions_for_markets(filtered_list)
|
|
476
|
+
# Group questions by market_id
|
|
477
|
+
questions_by_market: dict[HexBytes, list[SeerMarketQuestions]] = defaultdict(
|
|
478
|
+
list
|
|
479
|
+
)
|
|
480
|
+
for q in questions:
|
|
481
|
+
questions_by_market[q.market.id].append(q)
|
|
482
|
+
|
|
483
|
+
# Update the cache with the new questions for each market
|
|
484
|
+
for market_id, market_questions in questions_by_market.items():
|
|
485
|
+
self.market_id_to_questions[market_id] = market_questions
|