prediction-market-agent-tooling 0.68.0.dev999__py3-none-any.whl → 0.69.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/chains.py +1 -0
- prediction_market_agent_tooling/config.py +37 -2
- prediction_market_agent_tooling/deploy/agent.py +26 -21
- prediction_market_agent_tooling/deploy/betting_strategy.py +133 -22
- prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +17 -20
- prediction_market_agent_tooling/markets/agent_market.py +27 -9
- prediction_market_agent_tooling/markets/blockchain_utils.py +3 -3
- prediction_market_agent_tooling/markets/markets.py +16 -0
- prediction_market_agent_tooling/markets/omen/data_models.py +3 -18
- prediction_market_agent_tooling/markets/omen/omen.py +26 -11
- prediction_market_agent_tooling/markets/omen/omen_contracts.py +2 -196
- prediction_market_agent_tooling/markets/omen/omen_resolving.py +2 -2
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +13 -11
- prediction_market_agent_tooling/markets/polymarket/api.py +35 -1
- 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 +33 -5
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +247 -18
- prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
- prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +2 -1
- prediction_market_agent_tooling/markets/seer/data_models.py +41 -6
- prediction_market_agent_tooling/markets/seer/price_manager.py +69 -1
- prediction_market_agent_tooling/markets/seer/seer.py +77 -26
- prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +71 -20
- prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +67 -0
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +17 -22
- prediction_market_agent_tooling/tools/contract.py +236 -4
- prediction_market_agent_tooling/tools/cow/cow_order.py +13 -8
- prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
- prediction_market_agent_tooling/tools/hexbytes_custom.py +3 -9
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +17 -5
- prediction_market_agent_tooling/tools/tokens/auto_deposit.py +2 -2
- prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
- prediction_market_agent_tooling/tools/web3_utils.py +9 -4
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/METADATA +8 -7
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/RECORD +41 -38
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -366
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/LICENSE +0 -0
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/WHEEL +0 -0
- {prediction_market_agent_tooling-0.68.0.dev999.dist-info → prediction_market_agent_tooling-0.69.0.dist-info}/entry_points.txt +0 -0
@@ -6,12 +6,14 @@ from urllib.parse import urljoin
|
|
6
6
|
import httpx
|
7
7
|
import tenacity
|
8
8
|
|
9
|
+
from prediction_market_agent_tooling.gtypes import ChecksumAddress, HexBytes
|
9
10
|
from prediction_market_agent_tooling.loggers import logger
|
10
11
|
from prediction_market_agent_tooling.markets.polymarket.data_models import (
|
11
12
|
POLYMARKET_FALSE_OUTCOME,
|
12
13
|
POLYMARKET_TRUE_OUTCOME,
|
13
14
|
PolymarketGammaResponse,
|
14
15
|
PolymarketGammaResponseDataItem,
|
16
|
+
PolymarketPositionResponse,
|
15
17
|
)
|
16
18
|
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
17
19
|
from prediction_market_agent_tooling.tools.httpx_cached_client import HttpxCachedClient
|
@@ -84,7 +86,7 @@ def get_polymarkets_with_pagination(
|
|
84
86
|
markets_to_add = []
|
85
87
|
for m in market_response.data:
|
86
88
|
# Some Polymarket markets are missing the markets field
|
87
|
-
if m.markets is None:
|
89
|
+
if m.markets is None or m.markets[0].clobTokenIds is None:
|
88
90
|
continue
|
89
91
|
if excluded_questions and m.title in excluded_questions:
|
90
92
|
continue
|
@@ -127,3 +129,35 @@ def get_polymarkets_with_pagination(
|
|
127
129
|
|
128
130
|
# Return exactly the number of items requested (in case we got more due to batch size)
|
129
131
|
return all_markets[:limit]
|
132
|
+
|
133
|
+
|
134
|
+
@tenacity.retry(
|
135
|
+
stop=tenacity.stop_after_attempt(2),
|
136
|
+
wait=tenacity.wait_fixed(1),
|
137
|
+
after=lambda x: logger.debug(
|
138
|
+
f"get_user_positions failed, attempt={x.attempt_number}."
|
139
|
+
),
|
140
|
+
)
|
141
|
+
def get_user_positions(
|
142
|
+
user_id: ChecksumAddress,
|
143
|
+
condition_ids: list[HexBytes] | None = None,
|
144
|
+
) -> list[PolymarketPositionResponse]:
|
145
|
+
"""Fetch a user's Polymarket positions; optionally filter by condition IDs."""
|
146
|
+
url = "https://data-api.polymarket.com/positions"
|
147
|
+
# ... rest of implementation ...
|
148
|
+
client: httpx.Client = HttpxCachedClient(ttl=timedelta(seconds=60)).get_client()
|
149
|
+
|
150
|
+
params = {
|
151
|
+
"user": user_id,
|
152
|
+
"market": ",".join([i.to_0x_hex() for i in condition_ids])
|
153
|
+
if condition_ids
|
154
|
+
else None,
|
155
|
+
"sortBy": "CASHPNL", # Available options: TOKENS, CURRENT, INITIAL, CASHPNL, PERCENTPNL, TITLE, RESOLVING, PRICE
|
156
|
+
}
|
157
|
+
params = {k: v for k, v in params.items() if v is not None}
|
158
|
+
|
159
|
+
response = client.get(url, params=params)
|
160
|
+
response.raise_for_status()
|
161
|
+
data = response.json()
|
162
|
+
items = [PolymarketPositionResponse.model_validate(d) for d in data]
|
163
|
+
return items
|
@@ -0,0 +1,156 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
from py_clob_client.client import ClobClient
|
5
|
+
from py_clob_client.clob_types import MarketOrderArgs, OrderType
|
6
|
+
from py_clob_client.order_builder.constants import BUY, SELL
|
7
|
+
from pydantic import BaseModel
|
8
|
+
from web3 import Web3
|
9
|
+
|
10
|
+
from prediction_market_agent_tooling.chains import POLYGON_CHAIN_ID
|
11
|
+
from prediction_market_agent_tooling.config import APIKeys, RPCConfig
|
12
|
+
from prediction_market_agent_tooling.gtypes import USD, HexBytes, OutcomeToken, Wei
|
13
|
+
from prediction_market_agent_tooling.loggers import logger
|
14
|
+
from prediction_market_agent_tooling.markets.polymarket.constants import (
|
15
|
+
CTF_EXCHANGE_POLYMARKET,
|
16
|
+
NEG_RISK_ADAPTER,
|
17
|
+
NEG_RISK_EXCHANGE,
|
18
|
+
POLYMARKET_TINY_BET_AMOUNT,
|
19
|
+
)
|
20
|
+
from prediction_market_agent_tooling.markets.polymarket.polymarket_contracts import (
|
21
|
+
PolymarketConditionalTokenContract,
|
22
|
+
USDCeContract,
|
23
|
+
)
|
24
|
+
from prediction_market_agent_tooling.tools.cow.cow_order import handle_allowance
|
25
|
+
|
26
|
+
HOST = "https://clob.polymarket.com"
|
27
|
+
|
28
|
+
|
29
|
+
class AllowanceResult(BaseModel):
|
30
|
+
balance: float
|
31
|
+
allowances: Dict[str, float]
|
32
|
+
|
33
|
+
|
34
|
+
class PolymarketPriceSideEnum(str, Enum):
|
35
|
+
BUY = "BUY"
|
36
|
+
SELL = "SELL"
|
37
|
+
|
38
|
+
|
39
|
+
class OrderStatusEnum(str, Enum):
|
40
|
+
MATCHED = "matched"
|
41
|
+
LIVE = "live"
|
42
|
+
DELAYED = "delayed"
|
43
|
+
UNMATCHED = "unmatched"
|
44
|
+
|
45
|
+
|
46
|
+
class CreateOrderResult(BaseModel):
|
47
|
+
errorMsg: str
|
48
|
+
orderID: str
|
49
|
+
transactionsHashes: list[HexBytes]
|
50
|
+
status: OrderStatusEnum
|
51
|
+
success: bool
|
52
|
+
|
53
|
+
|
54
|
+
class PriceResponse(BaseModel):
|
55
|
+
price: float
|
56
|
+
|
57
|
+
|
58
|
+
class ClobManager:
|
59
|
+
def __init__(self, api_keys: APIKeys):
|
60
|
+
self.api_keys = api_keys
|
61
|
+
self.clob_client = ClobClient(
|
62
|
+
HOST,
|
63
|
+
key=api_keys.bet_from_private_key.get_secret_value(),
|
64
|
+
chain_id=POLYGON_CHAIN_ID,
|
65
|
+
)
|
66
|
+
self.clob_client.set_api_creds(self.clob_client.create_or_derive_api_creds())
|
67
|
+
self.polygon_web3 = RPCConfig().get_polygon_web3()
|
68
|
+
self.__init_approvals(polygon_web3=self.polygon_web3)
|
69
|
+
|
70
|
+
def get_token_price(self, token_id: int, side: PolymarketPriceSideEnum) -> USD:
|
71
|
+
price_data = self.clob_client.get_price(token_id=token_id, side=side.value)
|
72
|
+
price_item = PriceResponse.model_validate(price_data)
|
73
|
+
return USD(price_item.price)
|
74
|
+
|
75
|
+
def _place_market_order(
|
76
|
+
self, token_id: int, amount: float, side: PolymarketPriceSideEnum
|
77
|
+
) -> CreateOrderResult:
|
78
|
+
"""Internal method to place a market order.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
token_id: The token ID to trade
|
82
|
+
amount: The amount to trade (USDC for BUY, token shares for SELL)
|
83
|
+
side: Either BUY or SELL
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
CreateOrderResult: The result of the order placement
|
87
|
+
|
88
|
+
Raises:
|
89
|
+
ValueError: If usdc_amount is < 1.0 for BUY orders
|
90
|
+
"""
|
91
|
+
if side == PolymarketPriceSideEnum.BUY and amount < 1.0:
|
92
|
+
raise ValueError(
|
93
|
+
f"usdc_amounts < 1.0 are not supported by Polymarket, got {amount}"
|
94
|
+
)
|
95
|
+
|
96
|
+
# We check allowances first
|
97
|
+
self.__init_approvals()
|
98
|
+
|
99
|
+
order_args = MarketOrderArgs(
|
100
|
+
token_id=str(token_id),
|
101
|
+
amount=amount,
|
102
|
+
side=side.value,
|
103
|
+
)
|
104
|
+
|
105
|
+
logger.info(f"Placing market order: {order_args}")
|
106
|
+
signed_order = self.clob_client.create_market_order(order_args)
|
107
|
+
resp = self.clob_client.post_order(signed_order, orderType=OrderType.FOK)
|
108
|
+
return CreateOrderResult.model_validate(resp)
|
109
|
+
|
110
|
+
def place_buy_market_order(
|
111
|
+
self, token_id: int, usdc_amount: USD
|
112
|
+
) -> CreateOrderResult:
|
113
|
+
"""Place a market buy order for the given token with the specified USDC amount."""
|
114
|
+
return self._place_market_order(token_id, usdc_amount.value, BUY)
|
115
|
+
|
116
|
+
def place_sell_market_order(
|
117
|
+
self, token_id: int, token_shares: OutcomeToken
|
118
|
+
) -> CreateOrderResult:
|
119
|
+
"""Place a market sell order for the given token with the specified number of shares."""
|
120
|
+
return self._place_market_order(token_id, token_shares.value, SELL)
|
121
|
+
|
122
|
+
def __init_approvals(
|
123
|
+
self,
|
124
|
+
polygon_web3: Web3 | None = None,
|
125
|
+
) -> None:
|
126
|
+
# from https://github.com/Polymarket/agents/blob/main/agents/polymarket/polymarket.py#L341
|
127
|
+
polygon_web3 = polygon_web3 or self.polygon_web3
|
128
|
+
|
129
|
+
usdc = USDCeContract()
|
130
|
+
|
131
|
+
# When setting allowances on Polymarket, it's important to set a large amount, because
|
132
|
+
# every trade reduces the allowance by the amount of the trade.
|
133
|
+
large_amount_wei = Wei(int(100 * 1e6)) # 100 USDC in Wei
|
134
|
+
amount_to_check_wei = Wei(int(POLYMARKET_TINY_BET_AMOUNT.value * 1e6))
|
135
|
+
ctf = PolymarketConditionalTokenContract()
|
136
|
+
|
137
|
+
for target_address in [
|
138
|
+
CTF_EXCHANGE_POLYMARKET,
|
139
|
+
NEG_RISK_EXCHANGE,
|
140
|
+
NEG_RISK_ADAPTER,
|
141
|
+
]:
|
142
|
+
logger.info(f"Checking allowances for {target_address}")
|
143
|
+
handle_allowance(
|
144
|
+
api_keys=self.api_keys,
|
145
|
+
sell_token=usdc.address,
|
146
|
+
for_address=target_address,
|
147
|
+
amount_to_check_wei=amount_to_check_wei,
|
148
|
+
amount_to_set_wei=large_amount_wei,
|
149
|
+
web3=polygon_web3,
|
150
|
+
)
|
151
|
+
|
152
|
+
ctf.approve_if_not_approved(
|
153
|
+
api_keys=self.api_keys,
|
154
|
+
for_address=target_address,
|
155
|
+
web3=polygon_web3,
|
156
|
+
)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from web3 import Web3
|
2
|
+
|
3
|
+
from prediction_market_agent_tooling.gtypes import USD
|
4
|
+
|
5
|
+
CTF_EXCHANGE_POLYMARKET = Web3.to_checksum_address(
|
6
|
+
"0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e"
|
7
|
+
)
|
8
|
+
NEG_RISK_EXCHANGE = Web3.to_checksum_address(
|
9
|
+
"0xC5d563A36AE78145C45a50134d48A1215220f80a"
|
10
|
+
)
|
11
|
+
NEG_RISK_ADAPTER = Web3.to_checksum_address(
|
12
|
+
"0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296"
|
13
|
+
)
|
14
|
+
# We reference this value in multiple files
|
15
|
+
POLYMARKET_TINY_BET_AMOUNT = USD(1.0)
|
@@ -4,14 +4,14 @@ from pydantic import BaseModel
|
|
4
4
|
|
5
5
|
from prediction_market_agent_tooling.gtypes import USDC, OutcomeStr, Probability
|
6
6
|
from prediction_market_agent_tooling.markets.data_models import Resolution
|
7
|
-
from prediction_market_agent_tooling.markets.polymarket.data_models_web import (
|
8
|
-
POLYMARKET_FALSE_OUTCOME,
|
9
|
-
POLYMARKET_TRUE_OUTCOME,
|
10
|
-
construct_polymarket_url,
|
11
|
-
)
|
12
7
|
from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
|
13
8
|
from prediction_market_agent_tooling.tools.utils import DatetimeUTC
|
14
9
|
|
10
|
+
POLYMARKET_TRUE_OUTCOME = "Yes"
|
11
|
+
POLYMARKET_FALSE_OUTCOME = "No"
|
12
|
+
|
13
|
+
POLYMARKET_BASE_URL = "https://polymarket.com"
|
14
|
+
|
15
15
|
|
16
16
|
class PolymarketRewards(BaseModel):
|
17
17
|
min_size: int
|
@@ -39,6 +39,13 @@ class PolymarketGammaMarket(BaseModel):
|
|
39
39
|
questionId: str | None = None
|
40
40
|
clobTokenIds: str | None = None # int-encoded hex
|
41
41
|
|
42
|
+
@property
|
43
|
+
def token_ids(self) -> list[int]:
|
44
|
+
# If market has no token_ids, we halt for safety since it will fail later on.
|
45
|
+
if not self.clobTokenIds:
|
46
|
+
raise ValueError("Market has no token_ids")
|
47
|
+
return [int(i) for i in json.loads(self.clobTokenIds)]
|
48
|
+
|
42
49
|
@property
|
43
50
|
def outcomes_list(self) -> list[OutcomeStr]:
|
44
51
|
return [OutcomeStr(i) for i in json.loads(self.outcomes)]
|
@@ -186,3 +193,24 @@ class PolymarketMarketWithPrices(PolymarketMarket):
|
|
186
193
|
raise ValueError(
|
187
194
|
"Should not happen, as we filter only for binary markets in get_polymarket_binary_markets."
|
188
195
|
)
|
196
|
+
|
197
|
+
|
198
|
+
class PolymarketPositionResponse(BaseModel):
|
199
|
+
slug: str
|
200
|
+
eventSlug: str
|
201
|
+
proxyWallet: str
|
202
|
+
asset: str
|
203
|
+
conditionId: str
|
204
|
+
size: float
|
205
|
+
currentValue: float
|
206
|
+
cashPnl: float
|
207
|
+
redeemable: bool
|
208
|
+
outcome: str
|
209
|
+
outcomeIndex: int
|
210
|
+
|
211
|
+
|
212
|
+
def construct_polymarket_url(slug: str) -> str:
|
213
|
+
"""
|
214
|
+
Note: This works only if it's a single main market, not sub-market of some more general question.
|
215
|
+
"""
|
216
|
+
return f"{POLYMARKET_BASE_URL}/event/{slug}"
|
@@ -1,11 +1,19 @@
|
|
1
1
|
import typing as t
|
2
2
|
|
3
|
+
import cachetools
|
4
|
+
from web3 import Web3
|
5
|
+
|
6
|
+
from prediction_market_agent_tooling.config import APIKeys, RPCConfig
|
3
7
|
from prediction_market_agent_tooling.gtypes import (
|
4
8
|
USD,
|
9
|
+
ChecksumAddress,
|
5
10
|
CollateralToken,
|
6
11
|
HexBytes,
|
7
12
|
OutcomeStr,
|
13
|
+
OutcomeToken,
|
8
14
|
Probability,
|
15
|
+
Wei,
|
16
|
+
xDai,
|
9
17
|
)
|
10
18
|
from prediction_market_agent_tooling.loggers import logger
|
11
19
|
from prediction_market_agent_tooling.markets.agent_market import (
|
@@ -13,27 +21,45 @@ from prediction_market_agent_tooling.markets.agent_market import (
|
|
13
21
|
ConditionalFilterType,
|
14
22
|
FilterBy,
|
15
23
|
MarketFees,
|
24
|
+
ProcessedMarket,
|
16
25
|
QuestionType,
|
17
26
|
SortBy,
|
18
27
|
)
|
19
|
-
from prediction_market_agent_tooling.markets.data_models import
|
28
|
+
from prediction_market_agent_tooling.markets.data_models import (
|
29
|
+
ExistingPosition,
|
30
|
+
Resolution,
|
31
|
+
)
|
20
32
|
from prediction_market_agent_tooling.markets.polymarket.api import (
|
21
33
|
PolymarketOrderByEnum,
|
22
34
|
get_polymarkets_with_pagination,
|
35
|
+
get_user_positions,
|
36
|
+
)
|
37
|
+
from prediction_market_agent_tooling.markets.polymarket.clob_manager import (
|
38
|
+
ClobManager,
|
39
|
+
PolymarketPriceSideEnum,
|
40
|
+
)
|
41
|
+
from prediction_market_agent_tooling.markets.polymarket.constants import (
|
42
|
+
POLYMARKET_TINY_BET_AMOUNT,
|
23
43
|
)
|
24
44
|
from prediction_market_agent_tooling.markets.polymarket.data_models import (
|
45
|
+
POLYMARKET_BASE_URL,
|
25
46
|
PolymarketGammaResponseDataItem,
|
26
47
|
)
|
27
|
-
from prediction_market_agent_tooling.markets.polymarket.
|
28
|
-
|
48
|
+
from prediction_market_agent_tooling.markets.polymarket.polymarket_contracts import (
|
49
|
+
USDCeContract,
|
29
50
|
)
|
30
51
|
from prediction_market_agent_tooling.markets.polymarket.polymarket_subgraph_handler import (
|
31
52
|
ConditionSubgraphModel,
|
32
53
|
PolymarketSubgraphHandler,
|
33
54
|
)
|
34
55
|
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
56
|
+
from prediction_market_agent_tooling.tools.tokens.usd import get_token_in_usd
|
35
57
|
from prediction_market_agent_tooling.tools.utils import check_not_none
|
36
58
|
|
59
|
+
SHARED_CACHE: cachetools.TTLCache[t.Hashable, t.Any] = cachetools.TTLCache(
|
60
|
+
maxsize=256, ttl=10 * 60
|
61
|
+
)
|
62
|
+
|
37
63
|
|
38
64
|
class PolymarketAgentMarket(AgentMarket):
|
39
65
|
"""
|
@@ -47,6 +73,15 @@ class PolymarketAgentMarket(AgentMarket):
|
|
47
73
|
# But then in the new subgraph API, they have `fee: BigInt! (Percentage fee of trades taken by market maker. A 2% fee is represented as 2*10^16)`.
|
48
74
|
# TODO: Check out the fees while integrating the subgraph API or if we implement placing of bets on Polymarket.
|
49
75
|
fees: MarketFees = MarketFees.get_zero_fees()
|
76
|
+
condition_id: HexBytes
|
77
|
+
liquidity_usd: USD
|
78
|
+
token_ids: list[int]
|
79
|
+
closed_flag_from_polymarket: bool
|
80
|
+
active_flag_from_polymarket: bool
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def collateral_token_address() -> ChecksumAddress:
|
84
|
+
return USDCeContract().address
|
50
85
|
|
51
86
|
@staticmethod
|
52
87
|
def build_resolution_from_condition(
|
@@ -73,7 +108,7 @@ class PolymarketAgentMarket(AgentMarket):
|
|
73
108
|
if len(payout_numerator_indices_gt_0) != 1:
|
74
109
|
# These cases involve multi-categorical resolution (to be implemented https://github.com/gnosis/prediction-market-agent-tooling/issues/770)
|
75
110
|
logger.warning(
|
76
|
-
f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators} for condition_id {condition_id.
|
111
|
+
f"Only binary markets are supported. Got payout numerators: {condition_model.payoutNumerators} for condition_id {condition_id.to_0x_hex()}"
|
77
112
|
)
|
78
113
|
return Resolution(outcome=None, invalid=False)
|
79
114
|
|
@@ -81,42 +116,68 @@ class PolymarketAgentMarket(AgentMarket):
|
|
81
116
|
resolved_outcome = outcomes[payout_numerator_indices_gt_0[0]]
|
82
117
|
return Resolution.from_answer(resolved_outcome)
|
83
118
|
|
119
|
+
def get_token_id_for_outcome(self, outcome: OutcomeStr) -> int:
|
120
|
+
outcome_idx = self.outcomes.index(outcome)
|
121
|
+
return self.token_ids[outcome_idx]
|
122
|
+
|
84
123
|
@staticmethod
|
85
124
|
def from_data_model(
|
86
125
|
model: PolymarketGammaResponseDataItem,
|
87
126
|
condition_model_dict: dict[HexBytes, ConditionSubgraphModel],
|
88
|
-
) -> "PolymarketAgentMarket":
|
127
|
+
) -> t.Optional["PolymarketAgentMarket"]:
|
89
128
|
# If len(model.markets) > 0, this denotes a categorical market.
|
90
129
|
markets = check_not_none(model.markets)
|
91
130
|
outcomes = markets[0].outcomes_list
|
92
131
|
outcome_prices = markets[0].outcome_prices
|
93
132
|
if not outcome_prices:
|
94
|
-
|
95
|
-
|
133
|
+
logger.info(f"Market has no outcome prices. Skipping. {model=}")
|
134
|
+
return None
|
135
|
+
|
96
136
|
probabilities = {o: Probability(op) for o, op in zip(outcomes, outcome_prices)}
|
97
137
|
|
138
|
+
condition_id = markets[0].conditionId
|
98
139
|
resolution = PolymarketAgentMarket.build_resolution_from_condition(
|
99
|
-
condition_id=
|
140
|
+
condition_id=condition_id,
|
100
141
|
condition_model_dict=condition_model_dict,
|
101
142
|
outcomes=outcomes,
|
102
143
|
)
|
103
144
|
|
104
145
|
return PolymarketAgentMarket(
|
105
146
|
id=model.id,
|
147
|
+
condition_id=condition_id,
|
106
148
|
question=model.title,
|
107
149
|
description=model.description,
|
108
150
|
outcomes=outcomes,
|
109
151
|
resolution=resolution,
|
110
152
|
created_time=model.startDate,
|
111
153
|
close_time=model.endDate,
|
154
|
+
closed_flag_from_polymarket=model.closed,
|
155
|
+
active_flag_from_polymarket=model.active,
|
112
156
|
url=model.url,
|
113
157
|
volume=CollateralToken(model.volume) if model.volume else None,
|
114
158
|
outcome_token_pool=None,
|
115
159
|
probabilities=probabilities,
|
160
|
+
liquidity_usd=USD(model.liquidity)
|
161
|
+
if model.liquidity is not None
|
162
|
+
else USD(0),
|
163
|
+
token_ids=markets[0].token_ids,
|
116
164
|
)
|
117
165
|
|
118
166
|
def get_tiny_bet_amount(self) -> CollateralToken:
|
119
|
-
|
167
|
+
return CollateralToken(POLYMARKET_TINY_BET_AMOUNT.value)
|
168
|
+
|
169
|
+
def get_token_in_usd(self, x: CollateralToken) -> USD:
|
170
|
+
return get_token_in_usd(x, self.collateral_token_address())
|
171
|
+
|
172
|
+
@staticmethod
|
173
|
+
def get_trade_balance(api_keys: APIKeys, web3: Web3 | None = None) -> USD:
|
174
|
+
usdc_balance_wei = USDCeContract().balanceOf(
|
175
|
+
for_address=api_keys.public_key, web3=web3
|
176
|
+
)
|
177
|
+
return USD(usdc_balance_wei.value * 1e-6)
|
178
|
+
|
179
|
+
def get_liquidity(self, web3: Web3 | None = None) -> CollateralToken:
|
180
|
+
return CollateralToken(self.liquidity_usd.value)
|
120
181
|
|
121
182
|
def place_bet(self, outcome: OutcomeStr, amount: USD) -> str:
|
122
183
|
raise NotImplementedError("TODO: Implement to allow betting on Polymarket.")
|
@@ -146,6 +207,7 @@ class PolymarketAgentMarket(AgentMarket):
|
|
146
207
|
match sort_by:
|
147
208
|
case SortBy.NEWEST:
|
148
209
|
order_by = PolymarketOrderByEnum.START_DATE
|
210
|
+
ascending = False
|
149
211
|
case SortBy.CLOSING_SOONEST:
|
150
212
|
ascending = True
|
151
213
|
order_by = PolymarketOrderByEnum.END_DATE
|
@@ -168,15 +230,182 @@ class PolymarketAgentMarket(AgentMarket):
|
|
168
230
|
)
|
169
231
|
|
170
232
|
condition_models = PolymarketSubgraphHandler().get_conditions(
|
171
|
-
condition_ids=
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
233
|
+
condition_ids=list(
|
234
|
+
set(
|
235
|
+
[
|
236
|
+
market.markets[0].conditionId
|
237
|
+
for market in markets
|
238
|
+
if market.markets is not None
|
239
|
+
]
|
240
|
+
)
|
241
|
+
)
|
176
242
|
)
|
177
243
|
condition_models_dict = {c.id: c for c in condition_models}
|
178
244
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
245
|
+
result_markets: list[PolymarketAgentMarket] = []
|
246
|
+
for m in markets:
|
247
|
+
market = PolymarketAgentMarket.from_data_model(m, condition_models_dict)
|
248
|
+
if market is not None:
|
249
|
+
result_markets.append(market)
|
250
|
+
return result_markets
|
251
|
+
|
252
|
+
def ensure_min_native_balance(
|
253
|
+
self,
|
254
|
+
min_required_balance: xDai,
|
255
|
+
multiplier: float = 3.0,
|
256
|
+
web3: Web3 | None = None,
|
257
|
+
) -> None:
|
258
|
+
balance_collateral = USDCeContract().balanceOf(
|
259
|
+
for_address=APIKeys().public_key, web3=web3
|
260
|
+
)
|
261
|
+
# USDC has 6 decimals, xDAI has 18. We convert from Wei into atomic units.
|
262
|
+
balance_collateral_atomic = CollateralToken(float(balance_collateral) / 1e6)
|
263
|
+
if balance_collateral_atomic < min_required_balance.as_token:
|
264
|
+
raise EnvironmentError(
|
265
|
+
f"USDC balance {balance_collateral_atomic} < {min_required_balance.as_token=}"
|
266
|
+
)
|
267
|
+
|
268
|
+
@staticmethod
|
269
|
+
def redeem_winnings(api_keys: APIKeys) -> None:
|
270
|
+
# ToDo - implement me - https://github.com/gnosis/prediction-market-agent-tooling/issues/824
|
271
|
+
pass
|
272
|
+
|
273
|
+
@staticmethod
|
274
|
+
def verify_operational_balance(api_keys: APIKeys) -> bool:
|
275
|
+
"""Method for checking if agent has enough funds to pay for gas fees."""
|
276
|
+
web3 = RPCConfig().get_polygon_web3()
|
277
|
+
pol_balance: Wei = Wei(web3.eth.get_balance(api_keys.public_key))
|
278
|
+
return pol_balance > Wei(int(0.001 * 1e18))
|
279
|
+
|
280
|
+
def store_prediction(
|
281
|
+
self,
|
282
|
+
processed_market: ProcessedMarket | None,
|
283
|
+
keys: APIKeys,
|
284
|
+
agent_name: str,
|
285
|
+
) -> None:
|
286
|
+
pass
|
287
|
+
|
288
|
+
def store_trades(
|
289
|
+
self,
|
290
|
+
traded_market: ProcessedMarket | None,
|
291
|
+
keys: APIKeys,
|
292
|
+
agent_name: str,
|
293
|
+
web3: Web3 | None = None,
|
294
|
+
) -> None:
|
295
|
+
logger.info("Storing trades deactivated for Polymarket.")
|
296
|
+
# Understand how market_id can be represented.
|
297
|
+
# Condition_id could work but length doesn't seem to match.
|
298
|
+
|
299
|
+
@classmethod
|
300
|
+
def get_user_url(cls, keys: APIKeys) -> str:
|
301
|
+
return f"https://polymarket.com/{keys.public_key}"
|
302
|
+
|
303
|
+
def get_position(
|
304
|
+
self, user_id: str, web3: Web3 | None = None
|
305
|
+
) -> ExistingPosition | None:
|
306
|
+
"""
|
307
|
+
Fetches position from the user in a given market.
|
308
|
+
"""
|
309
|
+
positions = get_user_positions(
|
310
|
+
user_id=Web3.to_checksum_address(user_id), condition_ids=[self.condition_id]
|
311
|
+
)
|
312
|
+
if not positions:
|
313
|
+
return None
|
314
|
+
|
315
|
+
amounts_ot = {i: OutcomeToken(0) for i in self.outcomes}
|
316
|
+
amounts_potential = {i: USD(0) for i in self.outcomes}
|
317
|
+
amounts_current = {i: USD(0) for i in self.outcomes}
|
318
|
+
|
319
|
+
for p in positions:
|
320
|
+
if p.conditionId != self.condition_id.to_0x_hex():
|
321
|
+
continue
|
322
|
+
|
323
|
+
amounts_potential[OutcomeStr(p.outcome)] = USD(p.size)
|
324
|
+
amounts_ot[OutcomeStr(p.outcome)] = OutcomeToken(p.size)
|
325
|
+
amounts_current[OutcomeStr(p.outcome)] = USD(p.currentValue)
|
326
|
+
|
327
|
+
return ExistingPosition(
|
328
|
+
amounts_potential=amounts_potential,
|
329
|
+
amounts_ot=amounts_ot,
|
330
|
+
market_id=self.id,
|
331
|
+
amounts_current=amounts_current,
|
332
|
+
)
|
333
|
+
|
334
|
+
def can_be_traded(self) -> bool:
|
335
|
+
return (
|
336
|
+
self.active_flag_from_polymarket
|
337
|
+
and not self.closed_flag_from_polymarket
|
338
|
+
and self.liquidity_usd
|
339
|
+
> USD(5) # we conservatively require some positive liquidity to trade on
|
340
|
+
)
|
341
|
+
|
342
|
+
def get_buy_token_amount(
|
343
|
+
self, bet_amount: USD | CollateralToken, outcome_str: OutcomeStr
|
344
|
+
) -> OutcomeToken:
|
345
|
+
"""Returns number of outcome tokens returned for a given bet expressed in collateral units."""
|
346
|
+
|
347
|
+
if outcome_str not in self.outcomes:
|
348
|
+
raise ValueError(
|
349
|
+
f"Outcome {outcome_str} not found in market outcomes {self.outcomes}"
|
350
|
+
)
|
351
|
+
|
352
|
+
token_id = self.get_token_id_for_outcome(outcome_str)
|
353
|
+
|
354
|
+
price = ClobManager(APIKeys()).get_token_price(
|
355
|
+
token_id=token_id, side=PolymarketPriceSideEnum.BUY
|
356
|
+
)
|
357
|
+
if not price:
|
358
|
+
raise ValueError(
|
359
|
+
f"Could not get price for outcome {outcome_str} with token_id {token_id}"
|
360
|
+
)
|
361
|
+
|
362
|
+
# we work with floats since USD and Collateral are the same on Polymarket
|
363
|
+
buy_token_amount = bet_amount.value / price.value
|
364
|
+
logger.info(f"Buy token amount: {buy_token_amount=}")
|
365
|
+
return OutcomeToken(buy_token_amount)
|
366
|
+
|
367
|
+
def buy_tokens(self, outcome: OutcomeStr, amount: USD) -> str:
|
368
|
+
clob_manager = ClobManager(APIKeys())
|
369
|
+
token_id = self.get_token_id_for_outcome(outcome)
|
370
|
+
|
371
|
+
created_order = clob_manager.place_buy_market_order(
|
372
|
+
token_id=token_id, usdc_amount=amount
|
373
|
+
)
|
374
|
+
if not created_order.success:
|
375
|
+
raise ValueError(f"Error creating order: {created_order}")
|
376
|
+
|
377
|
+
return created_order.transactionsHashes[0].to_0x_hex()
|
378
|
+
|
379
|
+
def sell_tokens(
|
380
|
+
self,
|
381
|
+
outcome: OutcomeStr,
|
382
|
+
amount: USD | OutcomeToken,
|
383
|
+
api_keys: APIKeys | None = None,
|
384
|
+
) -> str:
|
385
|
+
"""
|
386
|
+
Polymarket's API expect shares to be sold. 1 share == 1 outcome token / 1e6.
|
387
|
+
The number of outcome tokens matches the `balanceOf` of the conditionalTokens contract.
|
388
|
+
In comparison, the number of shares match the position.size from the user position.
|
389
|
+
"""
|
390
|
+
logger.info(f"Selling {amount=} from {outcome=}")
|
391
|
+
clob_manager = ClobManager(api_keys=api_keys or APIKeys())
|
392
|
+
token_id = self.get_token_id_for_outcome(outcome)
|
393
|
+
token_shares: OutcomeToken
|
394
|
+
if isinstance(amount, OutcomeToken):
|
395
|
+
token_shares = amount
|
396
|
+
elif isinstance(amount, USD):
|
397
|
+
token_price = clob_manager.get_token_price(
|
398
|
+
token_id=token_id, side=PolymarketPriceSideEnum.SELL
|
399
|
+
)
|
400
|
+
# We expect that our order sizes don't move the price too much.
|
401
|
+
token_shares = OutcomeToken(amount.value / token_price.value)
|
402
|
+
else:
|
403
|
+
raise ValueError(f"Unsupported amount type {type(amount)}")
|
404
|
+
|
405
|
+
created_order = clob_manager.place_sell_market_order(
|
406
|
+
token_id=token_id, token_shares=token_shares
|
407
|
+
)
|
408
|
+
if not created_order.success:
|
409
|
+
raise ValueError(f"Error creating order: {created_order}")
|
410
|
+
|
411
|
+
return created_order.transactionsHashes[0].to_0x_hex()
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from web3 import Web3
|
2
|
+
|
3
|
+
from prediction_market_agent_tooling.config import APIKeys
|
4
|
+
from prediction_market_agent_tooling.gtypes import ChecksumAddress
|
5
|
+
from prediction_market_agent_tooling.tools.contract import (
|
6
|
+
ConditionalTokenContract,
|
7
|
+
ContractERC20BaseClass,
|
8
|
+
ContractOnPolygonChain,
|
9
|
+
)
|
10
|
+
|
11
|
+
|
12
|
+
class USDCeContract(ContractERC20BaseClass, ContractOnPolygonChain):
|
13
|
+
# USDC.e is used by Polymarket.
|
14
|
+
address: ChecksumAddress = Web3.to_checksum_address(
|
15
|
+
"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
class PolymarketConditionalTokenContract(
|
20
|
+
ConditionalTokenContract, ContractOnPolygonChain
|
21
|
+
):
|
22
|
+
address: ChecksumAddress = Web3.to_checksum_address(
|
23
|
+
"0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
|
24
|
+
)
|
25
|
+
|
26
|
+
def approve_if_not_approved(
|
27
|
+
self, api_keys: APIKeys, for_address: ChecksumAddress, web3: Web3 | None = None
|
28
|
+
) -> None:
|
29
|
+
is_approved = self.isApprovedForAll(
|
30
|
+
owner=api_keys.public_key, for_address=for_address, web3=web3
|
31
|
+
)
|
32
|
+
if not is_approved:
|
33
|
+
self.setApprovalForAll(
|
34
|
+
api_keys=api_keys, for_address=for_address, approve=True, web3=web3
|
35
|
+
)
|