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 32 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 @@ -269,35 +269,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 @@ -18,15 +18,14 @@
gcp_function_is_active,
gcp_resolve_api_keys_secrets,
)
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
from prediction_market_agent_tooling.monitor.monitor_app import DEPLOYED_AGENT_TYPE_MAP
from prediction_market_agent_tooling.tools.utils import DatetimeWithTimezone, utcnow

MAX_AVAILABLE_MARKETS = 20


class DeployableAgent:
def __init__(self) -> None:
Expand Down Expand Up @@ -154,7 +153,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
43 changes: 33 additions & 10 deletions prediction_market_agent_tooling/markets/manifold/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from prediction_market_agent_tooling.markets.data_models import (
BetAmount,
Currency,
Resolution,
ResolvedBet,
)
from prediction_market_agent_tooling.markets.manifold.data_models import (
Expand All @@ -26,30 +25,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 Expand Up @@ -137,7 +160,7 @@ def manifold_to_generic_resolved_bet(bet: ManifoldBet) -> ResolvedBet:
if not market.resolutionTime:
raise ValueError(f"Market {market.id} has no resolution time.")

market_outcome = market.get_resolution_enum() == Resolution.YES
market_outcome = market.get_resolved_boolean_outcome()
return ResolvedBet(
amount=BetAmount(amount=bet.amount, currency=Currency.Mana),
outcome=bet.get_resolved_boolean_outcome(),
Expand Down
28 changes: 16 additions & 12 deletions prediction_market_agent_tooling/markets/manifold/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,22 @@ class ManifoldMarket(BaseModel):
creatorId: str
closeTime: datetime
createdTime: datetime
creatorAvatarUrl: str
creatorAvatarUrl: t.Optional[str] = None
creatorName: str
creatorUsername: str
isResolved: bool
resolution: t.Optional[str] = None
resolution: t.Optional[Resolution] = None
resolutionTime: t.Optional[datetime] = None
lastBetTime: t.Optional[datetime] = None
lastCommentTime: t.Optional[datetime] = None
lastUpdatedTime: datetime
mechanism: str
outcomeType: str
p: float
p: t.Optional[float] = None
pool: ManifoldPool
probability: Probability
slug: str
totalLiquidity: Mana
totalLiquidity: t.Optional[Mana] = None
uniqueBettorCount: int
url: str
volume: Mana
Expand All @@ -55,14 +55,19 @@ class ManifoldMarket(BaseModel):
def outcomes(self) -> list[str]:
return list(self.pool.model_fields.keys())

def get_resolution_enum(self) -> Resolution:
return Resolution(self.resolution)
def get_resolved_boolean_outcome(self) -> bool:
if self.resolution == Resolution.YES:
return True
elif self.resolution == Resolution.NO:
return False
else:
should_not_happen(f"Unexpected bet outcome string, '{self.resolution}'.")

def is_resolved_non_cancelled(self) -> bool:
return (
self.isResolved
and self.resolutionTime is not None
and self.get_resolution_enum() != Resolution.CANCEL
and self.resolution not in [Resolution.CANCEL, Resolution.MKT]
Copy link
Contributor

Choose a reason for hiding this comment

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

We you able to find some information about what MKT means? A quick google search didn't tell me anything relevant, so idk how we should process them. But probably good to just leave it as it is, just checking if you have more information.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not clear from the manifold api docs, but I had a look through examples and it seems to be a binary market that doesn't resolve to yes or no, but to a percentage. The condition for this seems to be something the creator of the market can decide.

Here's a list of some manifold markets that have resolved this way:
https://manifold.markets/Joshua/very-short-fuse-resolves-to-my-rati
https://manifold.markets/ScottAlexander/what-percentile-did-the-median-supe
https://manifold.markets/ScottAlexander/what-percentile-did-metaculus-get-i
https://manifold.markets/ScottAlexander/what-percentile-did-manifold-get-in
https://manifold.markets/KongoLandwalker/nomic-game
https://manifold.markets/Ziddletwix/exactly-how-much-will-dune-part-two
https://manifold.markets/cc6/how-much-will-manifest-2024-cost-fo
https://manifold.markets/mattyb/shortfuse-what-will-i-rate-the-kfc
https://manifold.markets/KeenenWatts/will-netflix-open-higher-than-58356
https://manifold.markets/KongoLandwalker/how-far-will-i-go-in-my-productivit
https://manifold.markets/ML12d1/swiss-popular-vote-on-030324-how-ma
https://manifold.markets/EricNeyman/how-many-people-will-attend-the-ber
https://manifold.markets/june/fun-market-structure-how-many-accou
https://manifold.markets/bohaska/how-many-days-later-will-ediths-nex
https://manifold.markets/probajoelistic/how-many-points-will-be-scored-in-s
https://manifold.markets/benjaminIkuta/if-i-get-demoted-will-it-be-because-57b513edc788
https://manifold.markets/benjaminIkuta/this-market-resolves-prob-but-if-so-d09d52ca4a9a
https://manifold.markets/ChristopherRandles/resolve-increases-20-for-each-space-31719721cd8a
https://manifold.markets/SemioticRivalry/vibebased-what-percentage-will-the
https://manifold.markets/mattyb/shortfuse-does-my-mom-know-whos-pla
https://manifold.markets/AWA/will-i-finish-reading-15-books-by-t
https://manifold.markets/Joshua/what-will-be-the-initial-metaculus
https://manifold.markets/benjaminIkuta/this-market-resolves-prob-but-if-so-8b0c529d9efe
https://manifold.markets/OanaDragomir/will-adhd-remain-catalogued-as-a-di
https://manifold.markets/benjaminIkuta/if-i-get-a-job-will-i-fix-my-sleep
https://manifold.markets/mariopasquato/will-i-find-a-robust-causal-discove

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not clear to me what happens to the bets made on a binary market that resolves to MKT

)

def __repr__(self) -> str:
Expand Down Expand Up @@ -139,16 +144,15 @@ class ManifoldBet(BaseModel):
orderAmount: t.Optional[Mana] = None
fills: t.Optional[list[ManifoldBetFills]] = None
createdTime: datetime
outcome: str
outcome: Resolution

def get_resolved_boolean_outcome(self) -> bool:
outcome = Resolution(self.outcome)
if outcome == Resolution.YES:
if self.outcome == Resolution.YES:
return True
elif outcome == Resolution.NO:
elif self.outcome == Resolution.NO:
return False
else:
should_not_happen(f"Unexpected bet outcome string, '{outcome.value}'.")
should_not_happen(f"Unexpected bet outcome string, '{self.outcome.value}'.")

def get_profit(self, market_outcome: bool) -> ProfitAmount:
profit = (
Expand Down
39 changes: 36 additions & 3 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.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,
resolution=model.resolution,
created_time=model.createdTime,
p_yes=Probability(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}")
Loading
Loading