Skip to content
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

Merged
merged 35 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
28cba51
Add 'sort_by' arg to get_binary_markets
evangriffiths Feb 26, 2024
23c71cc
Add 'created_after' arg to 'get_binary_markets'
evangriffiths Feb 26, 2024
dd0fc99
Replace hardcoded manifold code in monitor_app with generic AgentMark…
evangriffiths Feb 27, 2024
fb0d26c
Tidy
evangriffiths Feb 27, 2024
6b45ea5
Add filter_by arg to get_binary_markets
evangriffiths Feb 27, 2024
2100bfc
Fix omen p_yes calculation in corner case
evangriffiths Feb 27, 2024
29f67d9
Fix Manifold market monitoring
evangriffiths Feb 27, 2024
3a528d0
Merge main
evangriffiths Feb 27, 2024
facd307
Fix mypy errors
evangriffiths Feb 27, 2024
c793a7a
Review comment
evangriffiths Feb 27, 2024
b108f00
Review comment
evangriffiths Feb 27, 2024
5a4cfc4
Update prediction_market_agent_tooling/markets/manifold/manifold.py
evangriffiths Feb 27, 2024
e2bd74d
Fixed
evangriffiths Feb 27, 2024
c8c0ded
black formatter
evangriffiths Feb 27, 2024
d6143fd
Fix offset
evangriffiths Feb 27, 2024
80e64c4
Fix
evangriffiths Feb 27, 2024
e99b12e
Merge main
evangriffiths Mar 4, 2024
9c0dc14
Add clarifying comment to calculation
evangriffiths Mar 4, 2024
63f689f
Merge main
evangriffiths Mar 5, 2024
818e3f0
Add more tests, tidy Resolution -> bool transform for bets
evangriffiths Mar 5, 2024
ccbc7b2
Fix mypy
evangriffiths Mar 5, 2024
4d54733
Remove harcoded market type
evangriffiths Mar 5, 2024
55e9b84
Improve omen market filtering
evangriffiths Mar 5, 2024
e7dd8a8
Fix mypy
evangriffiths Mar 5, 2024
505ab29
Spelling
evangriffiths Mar 6, 2024
7973825
Fix test_manifold.py
evangriffiths Mar 6, 2024
e2417d4
Add debug logging
evangriffiths Mar 6, 2024
8d606ce
Revert "Add debug logging"
evangriffiths Mar 6, 2024
692a96e
Tidy tests
evangriffiths Mar 6, 2024
52f3b99
Remove OmenBetFPMM class
evangriffiths Mar 6, 2024
2c22491
Remove hardcoded market monitoring
evangriffiths Mar 6, 2024
29f8746
Merge main
evangriffiths Mar 6, 2024
6a7bebc
Fix missing import after merge
evangriffiths Mar 6, 2024
4cccb0b
Fix mypy
evangriffiths Mar 6, 2024
9a9be8b
More efficient iterating though manifold markets when filtering by cr…
evangriffiths Mar 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions prediction_market_agent_tooling/benchmark/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,35 +258,6 @@ def get_manifold_markets_paged(
return markets


def get_manifold_markets_dated(
oldest_date: datetime,
filter_: t.Literal[
"open", "closed", "resolved", "closing-this-month", "closing-next-month"
] = "open",
excluded_questions: set[str] | None = None,
) -> t.List[Market]:
markets: list[Market] = []

offset = 0
while True:
new_markets = get_manifold_markets(
limit=MANIFOLD_API_LIMIT,
offset=offset,
filter_=filter_,
sort="newest", # Enforce sorting by newest, because there aren't date filters on the API.
)
if not new_markets:
break
for market in new_markets:
if market.created_time < oldest_date:
return markets
if not excluded_questions or market.question not in excluded_questions:
markets.append(market)
offset += 1

return markets


def get_polymarket_markets(
limit: int = 100,
active: bool | None = True,
Expand Down
18 changes: 12 additions & 6 deletions prediction_market_agent_tooling/deploy/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@
schedule_deployed_gcp_function,
)
from prediction_market_agent_tooling.deploy.gcp.utils import gcp_function_is_active
from prediction_market_agent_tooling.markets.agent_market import AgentMarket
from prediction_market_agent_tooling.markets.agent_market import AgentMarket, SortBy
from prediction_market_agent_tooling.markets.data_models import BetAmount
from prediction_market_agent_tooling.markets.markets import (
MarketType,
get_binary_markets,
)
from prediction_market_agent_tooling.markets.markets import MARKET_TYPE_MAP, MarketType

MAX_AVAILABLE_MARKETS = 20


class DeployableAgent:
Expand Down Expand Up @@ -120,7 +119,14 @@ def calculate_bet_amount(self, answer: bool, market: AgentMarket) -> BetAmount:
return market.get_tiny_bet_amount()

def run(self, market_type: MarketType, _place_bet: bool = True) -> None:
available_markets = get_binary_markets(market_type)
cls = MARKET_TYPE_MAP.get(market_type)
if not cls:
raise ValueError(f"Unknown market type: {market_type}")

# Fetch the soonest closing markets to choose from
available_markets = cls.get_binary_markets(
limit=MAX_AVAILABLE_MARKETS, sort_by=SortBy.CLOSING_SOONEST
)
markets = self.pick_markets(available_markets)
for market in markets:
result = self.answer_binary_market(market)
Expand Down
33 changes: 31 additions & 2 deletions prediction_market_agent_tooling/markets/agent_market.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import typing as t
from datetime import datetime
from decimal import Decimal
from enum import Enum

from pydantic import BaseModel

from prediction_market_agent_tooling.gtypes import Probability
from prediction_market_agent_tooling.markets.data_models import BetAmount, Currency
from prediction_market_agent_tooling.markets.data_models import (
BetAmount,
Currency,
Resolution,
)


class SortBy(str, Enum):
CLOSING_SOONEST = "closing-soonest"
NEWEST = "newest"


class FilterBy(str, Enum):
OPEN = "open"
RESOLVED = "resolved"


class AgentMarket(BaseModel):
Expand All @@ -18,6 +34,8 @@ class AgentMarket(BaseModel):
id: str
question: str
outcomes: list[str]
resolution: t.Optional[Resolution] = None
created_time: datetime
p_yes: Probability

@property
Expand All @@ -34,9 +52,20 @@ def place_bet(self, outcome: bool, amount: BetAmount) -> None:
raise NotImplementedError("Subclasses must implement this method")

@staticmethod
def get_binary_markets(limit: int) -> list["AgentMarket"]:
def get_binary_markets(
limit: int,
sort_by: SortBy,
filter_by: FilterBy = FilterBy.OPEN,
created_after: t.Optional[datetime] = None,
) -> list["AgentMarket"]:
raise NotImplementedError("Subclasses must implement this method")

def is_resolved(self) -> bool:
return self.resolution is not None

def has_successful_resolution(self) -> bool:
return self.resolution in [Resolution.YES, Resolution.NO]

def get_outcome_str(self, outcome_index: int) -> str:
try:
return self.outcomes[outcome_index]
Expand Down
1 change: 1 addition & 0 deletions prediction_market_agent_tooling/markets/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Resolution(str, Enum):
YES = "YES"
NO = "NO"
CANCEL = "CANCEL"
MKT = "MKT"
Copy link
Contributor

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 uses is_resolved_non_cancelled and then does market_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

class CancelableMarketResolution(str, Enum):
    YES = "yes"
    NO = "no"
    CANCEL = "cancel"
    MKT = "mkt"

and then that's converted to

class MarketResolution(str, Enum):
    YES = "yes"
    NO = "no"

in probable_resolution? So we don't have to worry about other potential changes.

Copy link
Contributor Author

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':

class ManifoldBet(BaseModel):
    resolution: t.Optional[Resolution] = None

and the boolean yes/no resolution:

class ResolvedBet(Bet):
    market_outcome: bool

Also good spot, have accounted for Resolution.MKT cases now.



class BetAmount(BaseModel):
Expand Down
40 changes: 32 additions & 8 deletions prediction_market_agent_tooling/markets/manifold/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,54 @@
Note: There is an existing wrapper here: https://github.com/vluzko/manifoldpy. Consider using that instead.
"""

MARKETS_LIMIT = 1000 # Manifold will only return up to 1000 markets


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: list[ManifoldMarket] = []

url = "https://api.manifold.markets/v0/search-markets"
params: dict[str, t.Union[str, int, float]] = {
"term": term,
"sort": sort,
"limit": limit,
"filter": "open",
"filter": filter_,
"limit": min(limit, MARKETS_LIMIT),
"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:
continue
all_markets.append(market)

if len(all_markets) >= limit:
break

offset += len(markets)

markets = [ManifoldMarket.model_validate(x) for x in data]
return markets
return all_markets[:limit]
Copy link
Contributor

@kongzii kongzii Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know get_manifold_markets, get_manifold_markets_paged and get_manifold_markets_dated from the benchmark code are a little unpractical, but that's because of API limitations; I think I liked that more. Could that code be moved over here? Because here we will have to do some runtime checks. But if I am too much biased as I wrote that code, please resist 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 get_manifold_markets and always replace its use with get_manifold_markets_paged, but what don't you like about combining with get_manifold_markets_dated as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was worried that if we do if created_after and market.createdTime < created_after: without enforcing "newest" sort, it's not clear how much time this function will take. As the markets can be sorted arbitrarily.

If we implement created_after like this, there is no reason to not have created_before as well, but then, with a big enough limit, the function could iterate the whole market database.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 AgentMarket.get_binary_markets to call the function responsibly. If you sort by newest, then atm the performance should be the exact same as with get_manifold_markets_dated. In terms of timing, looks like the penalty of iterating through the markets inefficiently (i.e. if markets are sorted by date) is small relative to overall time to get the response from manifold in the first place:

import time
from prediction_market_agent_tooling.markets.manifold.api import get_manifold_binary_markets

def time_get_markets(limit: int, sort: str):
    start_time = time.time()
    markets = get_manifold_binary_markets(limit=limit, sort=sort, filter_="resolved")
    print(f"Time for {sort}-sorted:", time.time() - start_time)
    assert len(markets) == limit

limit = 5000
time_get_markets(limit, "newest")
time_get_markets(limit, "liquidity")

Running a few times:

(.venv) prediction-market-agent-tooling % python ~/scratch/get_mani_time.py
Time for newest-sorted: 3.6355371475219727
(.venv) prediction-market-agent-tooling % python ~/scratch/get_mani_time.py
Time for newest-sorted: 9.79013991355896
Time for liquidity-sorted: 4.252532958984375
(.venv) prediction-market-agent-tooling % python ~/scratch/get_mani_time.py
Time for newest-sorted: 3.142300844192505
Time for liquidity-sorted: 3.7035253047943115
(.venv) prediction-market-agent-tooling % python ~/scratch/get_mani_time.py
Time for newest-sorted: 3.22481107711792
Time for liquidity-sorted: 3.2679333686828613

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enhancements to the get_manifold_binary_markets function, including the addition of new parameters (sort, filter_, created_after) and the implementation of pagination logic, significantly improve the function's flexibility and efficiency. The use of pagination ensures that the function can handle large sets of data without overwhelming the API or the application. However, the function lacks error handling for the API requests, which could lead to unhandled exceptions if the API call fails.

Consider adding error handling for the API requests to gracefully handle failures and provide meaningful feedback to the caller.



def pick_binary_market() -> ManifoldMarket:
Expand Down
43 changes: 38 additions & 5 deletions prediction_market_agent_tooling/markets/manifold/manifold.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import typing as t
from datetime import datetime
from decimal import Decimal
from math import ceil

from prediction_market_agent_tooling.gtypes import Mana, Probability, mana_type
from prediction_market_agent_tooling.markets.agent_market import AgentMarket
from prediction_market_agent_tooling.gtypes import Mana, mana_type
from prediction_market_agent_tooling.markets.agent_market import (
AgentMarket,
FilterBy,
SortBy,
)
from prediction_market_agent_tooling.markets.betting_strategies import (
minimum_bet_to_win,
)
Expand Down Expand Up @@ -44,12 +49,40 @@ def from_data_model(model: ManifoldMarket) -> "ManifoldAgentMarket":
id=model.id,
question=model.question,
outcomes=model.outcomes,
p_yes=Probability(model.pool.YES),
resolution=model.get_resolution_enum() if model.isResolved else None,
created_time=model.createdTime,
p_yes=model.probability,
)

@staticmethod
def get_binary_markets(limit: int) -> list[AgentMarket]:
def get_binary_markets(
limit: int,
sort_by: SortBy,
filter_by: FilterBy = FilterBy.OPEN,
created_after: t.Optional[datetime] = None,
) -> list[AgentMarket]:
sort: t.Literal["newest", "close-date"]
if sort_by == SortBy.CLOSING_SOONEST:
sort = "close-date"
elif sort_by == SortBy.NEWEST:
sort = "newest"
else:
raise ValueError(f"Unknown sort_by: {sort_by}")

filter_: t.Literal["open", "resolved"]
if filter_by == FilterBy.OPEN:
filter_ = "open"
elif filter_by == FilterBy.RESOLVED:
filter_ = "resolved"
else:
raise ValueError(f"Unknown filter_by: {filter_by}")

return [
ManifoldAgentMarket.from_data_model(m)
for m in get_manifold_binary_markets(limit=limit, sort="close-date")
for m in get_manifold_binary_markets(
limit=limit,
sort=sort,
created_after=created_after,
filter_=filter_,
)
]
8 changes: 0 additions & 8 deletions prediction_market_agent_tooling/markets/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,3 @@ class MarketType(str, Enum):
MarketType.MANIFOLD: ManifoldAgentMarket,
MarketType.OMEN: OmenAgentMarket,
}


def get_binary_markets(market_type: MarketType, limit: int = 20) -> list[AgentMarket]:
cls = MARKET_TYPE_MAP.get(market_type)
if cls:
return cls.get_binary_markets(limit=limit)
else:
raise ValueError(f"Unknown market type: {market_type}")
78 changes: 69 additions & 9 deletions prediction_market_agent_tooling/markets/omen/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
BetAmount,
Currency,
ProfitAmount,
Resolution,
ResolvedBet,
)
from prediction_market_agent_tooling.tools.utils import check_not_none
Expand Down Expand Up @@ -50,6 +51,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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of optional properties (resolutionTimestamp, answerFinalizedTimestamp, currentAnswer, creationTimestamp) to the OmenMarket class enhances the model's flexibility to represent various states of a market. Ensure these properties are thoroughly tested, especially in scenarios where they might be None.

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:
Expand All @@ -75,30 +90,73 @@ def outcomeTokenProbabilities(self) -> t.Optional[list[Probability]]:
else None
)

@property
def p_yes(self) -> Probability:
return check_not_none(
self.outcomeTokenProbabilities,
"outcomeTokenProbabilities not available",
)[self.outcomes.index(OMEN_TRUE_OUTCOME)]

@property
def p_no(self) -> Probability:
return Probability(1 - self.p_yes)

@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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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:
return Resolution.NO


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
Expand Down Expand Up @@ -179,6 +237,8 @@ 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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over fix from previous PR!

check_not_none(self.fpmm.resolutionTimestamp)
),
profit=self.get_profit(),
)
Loading
Loading