208 lines
8.3 KiB
Python
208 lines
8.3 KiB
Python
"""Contains rules that reference other Manifold markets."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from time import time
|
|
from typing import TYPE_CHECKING, cast
|
|
|
|
from attrs import define
|
|
from pymanifold.utils.math import prob_to_number_cpmm1
|
|
|
|
from ... import Rule
|
|
from ...caching import parallel
|
|
from ...consts import BinaryResolution, Outcome, T
|
|
from ...util import market_to_answer_map, round_sig_figs
|
|
from .. import DoResolveRule
|
|
from ..abstract import ResolveRandomSeed
|
|
from . import ManifoldMarketMixin
|
|
|
|
if TYPE_CHECKING: # pragma: no cover
|
|
from typing import Any
|
|
|
|
from pymanifold.types import Market as APIMarket
|
|
|
|
from ...account import Account
|
|
from ...consts import AnyResolution
|
|
from ...market import Market
|
|
|
|
|
|
@define(slots=False)
|
|
class OtherMarketClosed(DoResolveRule, ManifoldMarketMixin):
|
|
"""A rule that checks whether another market is closed."""
|
|
|
|
def _value(self, market: Market, account: Account) -> bool:
|
|
mkt = self.api_market(market=market)
|
|
assert mkt.closeTime is not None
|
|
return bool(mkt.isResolved or (mkt.closeTime < time() * 1000))
|
|
|
|
def _explain_abstract(self, indent: int = 0, **kwargs: Any) -> str:
|
|
return f"{' ' * indent}- If `{self.id_}` closes ({self.api_market().question}).\n"
|
|
|
|
|
|
@define(slots=False)
|
|
class OtherMarketResolved(DoResolveRule, ManifoldMarketMixin):
|
|
"""A rule that checks whether another market is resolved."""
|
|
|
|
def _value(self, market: Market, account: Account) -> bool:
|
|
return bool(self.api_market().isResolved)
|
|
|
|
def _explain_abstract(self, indent: int = 0, **kwargs: Any) -> str:
|
|
return f"{' ' * indent}- If `{self.id_}` is resolved ({self.api_market().question}).\n"
|
|
|
|
|
|
@define(slots=False)
|
|
class OtherMarketUniqueTraders(ManifoldMarketMixin, Rule[int]):
|
|
"""A rule that checks whether another market is resolved."""
|
|
|
|
def _value(self, market: Market, account: Account) -> int:
|
|
return len(
|
|
{bet.userId for bet in self.api_market(market=market).bets} - {None}
|
|
)
|
|
|
|
def _explain_abstract(self, indent: int = 0, **kwargs: Any) -> str:
|
|
return f"{' ' * indent}- The number of unique traders on `{self.id_}` ({self.api_market().question}).\n"
|
|
|
|
|
|
@define(slots=False)
|
|
class OtherMarketValue(Rule[T], ManifoldMarketMixin):
|
|
"""A rule that resolves to the value of another rule."""
|
|
|
|
def _value(self, market: Market, account: Account) -> T:
|
|
mkt = self.api_market(market=market)
|
|
if mkt.resolution == "CANCEL":
|
|
ret: AnyResolution = "CANCEL"
|
|
elif mkt.outcomeType == "BINARY":
|
|
ret = self._binary_value(market, mkt) * 100
|
|
elif mkt.outcomeType == "PSEUDO_NUMERIC":
|
|
ret = prob_to_number_cpmm1(
|
|
self._binary_value(market, mkt),
|
|
float(mkt.min or 0),
|
|
float(mkt.max or 0),
|
|
bool(mkt.isLogScale)
|
|
)
|
|
else:
|
|
ret = market_to_answer_map(mkt)
|
|
return cast(T, ret)
|
|
|
|
def _binary_value(self, market: Market, mkt: APIMarket | None = None) -> float:
|
|
if mkt is None:
|
|
mkt = self.api_market(market=market)
|
|
|
|
if mkt.resolution == "YES":
|
|
ret: float = True
|
|
elif mkt.resolution == "NO":
|
|
ret = False
|
|
elif mkt.resolutionProbability is not None:
|
|
ret = mkt.resolutionProbability
|
|
elif mkt.probability is not None:
|
|
ret = mkt.probability
|
|
else:
|
|
prob = mkt.bets[-1].probAfter
|
|
assert prob is not None
|
|
ret = prob
|
|
return ret
|
|
|
|
def _explain_abstract(self, indent: int = 0, **kwargs: Any) -> str:
|
|
return (f"{' ' * indent}- Resolved (or current, if not resolved) value of `{self.id_}` "
|
|
f"({self.api_market().question}).\n")
|
|
|
|
def _explain_specific(self, market: Market, account: Account, indent: int = 0, sig_figs: int = 4) -> str:
|
|
f_mkt = self.f_api_market(market=market)
|
|
f_val = parallel(self._value, market, account)
|
|
mkt = f_mkt.result()
|
|
if hasattr(self, 'id_'):
|
|
ret = (f"{' ' * indent}- Resolved (or current, if not resolved) value of `{self.id_}` "
|
|
f"({mkt.question}) (-> ")
|
|
else:
|
|
ret = f"{' ' * indent}- Resolved (or current, if not resolved) value of this market (-> "
|
|
val = f_val.result()
|
|
if val == "CANCEL":
|
|
ret += "CANCEL"
|
|
elif mkt.outcomeType == Outcome.PSEUDO_NUMERIC:
|
|
assert isinstance(val, float)
|
|
ret += f"{round_sig_figs(val, sig_figs)}"
|
|
elif mkt.outcomeType == Outcome.BINARY:
|
|
assert isinstance(val, float)
|
|
ret += f"{round_sig_figs(val, sig_figs)}%"
|
|
elif mkt.outcomeType in Outcome.MC_LIKE():
|
|
ret += repr(market_to_answer_map(mkt))
|
|
return ret + ")\n"
|
|
|
|
|
|
@define(slots=False)
|
|
class AmplifiedOddsRule(OtherMarketValue[BinaryResolution], ResolveRandomSeed):
|
|
"""Immitate the amplified odds scheme deployed by @Tetraspace.
|
|
|
|
This rule resolves YES if the referenced market resolves YES.
|
|
|
|
If the referenced market resolves NO, I will get a random number using a predetermined seed. If it is less than
|
|
`1 / a`, I will resolve NO. Otherwise, I will resolve N/A. This means that, for this rule, you should treat NO as
|
|
if it is `a` times less likely to happen than it actually is.
|
|
|
|
For example, if `a = 100`, and your actual expected outcome is 0.01% YES, 99.99% NO, you should expect this to
|
|
resolve with probabilities 0.01% YES, 0.9999% NO, 98.9901% N/A, which means that your price of a YES share should
|
|
be ~1% (actually 0.99%).
|
|
|
|
Some other values, for calibration (using the formula YES' = YES/(YES + (1-YES)/100), where YES' is the price for
|
|
this question, and YES is your actual probability):
|
|
0.02% YES => ~2% YES' (actually 1.96%)
|
|
0.05% YES => ~5% YES' (actually 4.76%)
|
|
0.1% YES => 9% YES'
|
|
0.2% YES => 17% YES'
|
|
0.5% YES => 33% YES'
|
|
1% YES => 50% YES'
|
|
2% YES => 67% YES'
|
|
5% YES => 84% YES'
|
|
10% YES => 92% YES'
|
|
20% YES => 96% YES'
|
|
50% YES => 99% YES'
|
|
100% YES => 100% YES'
|
|
"""
|
|
|
|
a: int = 1
|
|
|
|
def _value(self, market: Market, account: Account) -> BinaryResolution:
|
|
val = OtherMarketValue._binary_value(self, market)
|
|
if val is True:
|
|
return True
|
|
if val is False:
|
|
if ResolveRandomSeed._value(self, market, account) < (1 / self.a):
|
|
return False
|
|
return "CANCEL"
|
|
return val / (val + (1 - val) / self.a) * 100
|
|
|
|
def _explain_abstract(self, indent: int = 0, **kwargs: Any) -> str:
|
|
ret = f"{' ' * indent}- Amplified odds:\n"
|
|
indent += 1
|
|
ret += f"{' ' * indent}- If the referenced market resolves YES, resolve YES\n"
|
|
ret += super()._explain_abstract(indent + 1, **kwargs)
|
|
ret += f"{' ' * indent}- If it resolved NO, generate a random number using a predetermined seed\n"
|
|
indent += 1
|
|
a_recip = round_sig_figs(1 / self.a)
|
|
ret += f"{' ' * indent}- If the number is less than `1 / a` ({self.a} -> ~{a_recip}), resolve NO\n"
|
|
ret += f"{' ' * indent}- Otherwise, resolve N/A\n"
|
|
indent -= 1
|
|
ret += f"{' ' * indent}- Otherwise, resolve to the equivalent price of the reference market\n"
|
|
return ret
|
|
|
|
def _explain_specific(self, market: Market, account: Account, indent: int = 0, sig_figs: int = 4) -> str:
|
|
f_val = parallel(self._value, market, account)
|
|
other_exp = parallel(OtherMarketValue._explain_specific, self, market, account, indent + 1, sig_figs)
|
|
ret = f"{' ' * indent}- Amplified odds: (-> "
|
|
val = f_val.result()
|
|
if val == "CANCEL":
|
|
ret += "CANCEL)\n"
|
|
else:
|
|
ret += f"{round_sig_figs(val, 4)}%)\n"
|
|
indent += 1
|
|
ret += f"{' ' * indent}- If the referenced market resolves True, resolve True\n"
|
|
ret += other_exp.result()
|
|
ret += f"{' ' * indent}- If it resolved NO, generate a random number using a predetermined seed\n"
|
|
indent += 1
|
|
a_recip = round_sig_figs(1 / self.a, sig_figs)
|
|
ret += f"{' ' * indent}- If the number is less than `1 / a` ({self.a} -> ~{a_recip}), resolve NO\n"
|
|
ret += f"{' ' * indent}- Otherwise, resolve N/A\n"
|
|
indent -= 1
|
|
ret += f"{' ' * indent}- Otherwise, resolve to the equivalent price of the reference market\n"
|
|
return ret
|