-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable monitoring of Omen market #59
Changes from 7 commits
28cba51
23c71cc
dd0fc99
fb0d26c
6b45ea5
2100bfc
29f67d9
3a528d0
facd307
c793a7a
b108f00
5a4cfc4
e2bd74d
c8c0ded
d6143fd
80e64c4
e99b12e
9c0dc14
63f689f
818e3f0
ccbc7b2
4d54733
55e9b84
e7dd8a8
505ab29
7973825
e2417d4
8d606ce
692a96e
52f3b99
2c22491
29f8746
6a7bebc
4cccb0b
9a9be8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,8 @@ | |
ManifoldUser, | ||
) | ||
|
||
MARKETS_LIMIT = 1000 # Manifold will only return up to 1000 markets | ||
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
""" | ||
Python API for Manifold Markets | ||
|
||
|
@@ -31,25 +33,47 @@ def get_manifold_binary_markets( | |
limit: int, | ||
term: str = "", | ||
topic_slug: t.Optional[str] = None, | ||
sort: str = "liquidity", | ||
sort: t.Literal["liquidity", "score", "newest", "close-date"] = "liquidity", | ||
filter_: t.Literal[ | ||
"open", "closed", "resolved", "closing-this-month", "closing-next-month" | ||
] = "open", | ||
created_after: t.Optional[datetime] = None, | ||
) -> list[ManifoldMarket]: | ||
all_markets = [] | ||
|
||
url = "https://api.manifold.markets/v0/search-markets" | ||
params: dict[str, t.Union[str, int, float]] = { | ||
"term": term, | ||
"sort": sort, | ||
"filter": filter_, | ||
"limit": limit, | ||
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"filter": "open", | ||
"contractType": "BINARY", | ||
} | ||
if topic_slug: | ||
params["topicSlug"] = topic_slug | ||
response = requests.get(url, params=params) | ||
|
||
response.raise_for_status() | ||
data = response.json() | ||
offset = 0 | ||
while True: | ||
params["offset"] = offset | ||
response = requests.get(url, params=params) | ||
response.raise_for_status() | ||
data = response.json() | ||
markets = [ManifoldMarket.model_validate(x) for x in data] | ||
|
||
if not markets: | ||
break | ||
|
||
for market in markets: | ||
if created_after and market.createdTime < created_after: | ||
return all_markets[:limit] | ||
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
|
||
all_markets.append(market) | ||
|
||
if len(all_markets) >= limit: | ||
break | ||
|
||
offset += 1 | ||
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
markets = [ManifoldMarket.model_validate(x) for x in data] | ||
return markets | ||
return all_markets[:limit] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha ohhhhh I thought it was nice to get merge the 3 functions into 1. Makes sense to remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was worried that if we do If we implement That's why I liked the previous split that was based on the API limitations. However, if you don't think that's a real problem, maybe I'm just too worried so feel free to leave it like this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah good point. I guess I'm saying we can leave it up to the user of
Running a few times:
|
||
|
||
|
||
def pick_binary_market() -> ManifoldMarket: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ | |
BetAmount, | ||
Currency, | ||
ProfitAmount, | ||
Resolution, | ||
ResolvedBet, | ||
) | ||
from prediction_market_agent_tooling.tools.utils import check_not_none | ||
|
@@ -48,6 +49,20 @@ class OmenMarket(BaseModel): | |
outcomeTokenAmounts: list[OmenOutcomeToken] | ||
outcomeTokenMarginalPrices: t.Optional[list[xDai]] | ||
fee: t.Optional[Wei] | ||
resolutionTimestamp: t.Optional[int] = None | ||
answerFinalizedTimestamp: t.Optional[int] = None | ||
currentAnswer: t.Optional[str] = None | ||
creationTimestamp: t.Optional[int] = None | ||
Comment on lines
+56
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The addition of optional properties ( Would you like me to help with creating unit tests for these properties to ensure their accuracy? |
||
|
||
@property | ||
def is_open(self) -> bool: | ||
return self.currentAnswer is None | ||
|
||
@property | ||
def is_resolved(self) -> bool: | ||
return ( | ||
self.answerFinalizedTimestamp is not None and self.currentAnswer is not None | ||
) | ||
|
||
@property | ||
def market_maker_contract_address(self) -> HexAddress: | ||
|
@@ -83,19 +98,69 @@ def get_outcome_str(self, outcome_index: int) -> str: | |
else: | ||
return self.outcomes[outcome_index] | ||
|
||
@property | ||
def p_yes(self) -> Probability: | ||
""" | ||
Calculate the probability of the outcomes from the relative token amounts. | ||
|
||
Note, not all markets reliably have outcomeTokenMarginalPrices, hence the | ||
need for this method. | ||
""" | ||
if self.outcomeTokenAmounts is None: | ||
raise ValueError( | ||
f"Market with title {self.title} has no outcomeTokenAmounts." | ||
) | ||
if len(self.outcomeTokenAmounts) != 2: | ||
raise ValueError( | ||
f"Market with title {self.title} has {len(self.outcomeTokenAmounts)} outcomes." | ||
) | ||
true_index = self.outcomes.index(OMEN_TRUE_OUTCOME) | ||
|
||
if sum(self.outcomeTokenAmounts) == 0: | ||
return Probability(0.5) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't expect this 😄 |
||
|
||
return Probability( | ||
1 - self.outcomeTokenAmounts[true_index] / sum(self.outcomeTokenAmounts) | ||
) | ||
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def __repr__(self) -> str: | ||
return f"Omen's market: {self.title}" | ||
|
||
@property | ||
def is_binary(self) -> bool: | ||
return len(self.outcomes) == 2 | ||
|
||
@property | ||
def boolean_outcome(self) -> bool: | ||
if not self.is_binary: | ||
raise ValueError( | ||
f"Market with title {self.title} is not binary, it has {len(self.outcomes)} outcomes." | ||
) | ||
if not self.is_resolved: | ||
raise ValueError(f"Bet with title {self.title} is not resolved.") | ||
|
||
outcome: str = self.outcomes[int(check_not_none(self.currentAnswer), 16)] | ||
return get_boolean_outcome(outcome) | ||
|
||
def get_resolution_enum(self) -> t.Optional[Resolution]: | ||
if not self.is_resolved: | ||
return None | ||
if self.boolean_outcome: | ||
return Resolution.YES | ||
else: | ||
Resolution.NO | ||
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class OmenBetCreator(BaseModel): | ||
id: HexAddress | ||
|
||
|
||
class OmenBetFPMM(BaseModel): | ||
class OmenBetFPMM(BaseModel): # TODO replace with OmenMarket | ||
id: HexAddress | ||
outcomes: list[str] | ||
title: str | ||
answerFinalizedTimestamp: t.Optional[int] = None | ||
resolutionTimestamp: t.Optional[int] = None | ||
currentAnswer: t.Optional[str] = None | ||
isPendingArbitration: bool | ||
arbitrationOccurred: bool | ||
|
@@ -176,6 +241,6 @@ def to_generic_resolved_bet(self) -> ResolvedBet: | |
created_time=datetime.fromtimestamp(self.creationTimestamp), | ||
market_question=self.title, | ||
market_outcome=self.fpmm.boolean_outcome, | ||
resolved_time=datetime.fromtimestamp(self.fpmm.answerFinalizedTimestamp), # type: ignore # TODO Mypy doesn't understand that self.fpmm.is_resolved is True and therefore timestamp is known non-None | ||
resolved_time=datetime.fromtimestamp(self.fpmm.resolutionTimestamp), # type: ignore # TODO Mypy doesn't understand that self.fpmm.is_resolved is True and therefore timestamp is known non-None | ||
evangriffiths marked this conversation as resolved.
Show resolved
Hide resolved
|
||
profit=self.get_profit(), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is
manifold_to_generic_resolved_bet
that usesis_resolved_non_cancelled
and then doesmarket_outcome = market.get_resolution_enum() == Resolution.YES
, but this MKT is not accounted for.Maybe we could use pattern from benchmark, where the API's model has
and then that's converted to
in
probable_resolution
? So we don't have to worry about other potential changes.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm yeah it's a little messy. I have tidied this a bit now. I feel like we essentially do have that pattern now:
The 'cancellable resolution':
and the boolean yes/no resolution:
Also good spot, have accounted for
Resolution.MKT
cases now.