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

Provide support for multiple accounts, add a new state module #48

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.8.0.1
current_version = 0.8.0.9
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<build>\d+)
serialize = {major}.{minor}.{patch}.{build}

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pytest_args += --flake8 --isort --pydocstyle
endif

ifeq ($(LINT),only)
pytest_args += --ignore=./ManifoldMarketManager/test --ignore=./ManifoldMarketManager/PyManifold/tests
pytest_args += --ignore=./ManifoldMarketManager/test --ignore=./ManifoldMarketManager/PyManifold/tests --ignore=./ManifoldMarketManager/manibots
COV=false
endif

Expand Down Expand Up @@ -80,7 +80,7 @@ test_%:

.PHONY: _test
_test:
@source env_personal.sh && ManifoldMarketManager_NO_CACHE=1 PYTHONPATH=${PYTHONPATH}:./ManifoldMarketManager/PyManifold $(PY) -m pytest ManifoldMarketManager $(pytest_args) -k 'not mypy-status' --ignore=./ManifoldMarketManager/test/manifold
@source env_personal.sh && ManifoldMarketManager_NO_CACHE=1 PYTHONPATH=${PYTHONPATH}:./ManifoldMarketManager/PyManifold $(PY) -m pytest ManifoldMarketManager $(pytest_args) -k 'not mypy-status'

.PHONY: dependencies
ifeq ($(MYPY),true)
Expand Down
Binary file modified ManifoldMarketManager.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 13 additions & 10 deletions ManifoldMarketManager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

_sys_path.append(str(Path(__file__).parent.joinpath("PyManifold")))

from .account import Account # noqa: E402
from .caching import parallel # noqa: E402
from .consts import AnyResolution, Outcome, T # noqa: E402
from .util import DictDeserializable # noqa: E402
Expand Down Expand Up @@ -50,7 +51,8 @@ def __attrs_post_init__(self) -> None:
@abstractmethod
def _value(
self,
market: Market
market: Market,
account: Account
) -> T: # pragma: no cover
...

Expand All @@ -66,20 +68,21 @@ def __getstate__(self) -> Mapping[str, Any]:
def value(
self,
market: Market,
account: Account,
format: Literal['NONE'] | OutcomeType = 'NONE',
refresh: bool = False
) -> AnyResolution:
"""Return the resolution value of a market, appropriately formatted for its market type."""
ret = self._value(market)
ret = self._value(market, account)
if (ret is None) or (ret == "CANCEL") or (format == 'NONE'):
return cast(AnyResolution, ret)
elif format in Outcome.BINARY_LIKE():
return self.__binary_value(market, ret)
return self.__binary_value(market, account, ret)
elif format in Outcome.MC_LIKE():
return self.__multiple_choice_value(market, ret)
return self.__multiple_choice_value(market, account, ret)
raise ValueError()

def __binary_value(self, market: Market, ret: Any) -> float:
def __binary_value(self, market: Market, account: Account, ret: Any) -> float:
if not isinstance(ret, str) and isinstance(ret, Sequence):
(ret, ) = ret
elif isinstance(ret, Mapping) and len(ret) == 1:
Expand All @@ -92,7 +95,7 @@ def __binary_value(self, market: Market, ret: Any) -> float:

raise TypeError(ret, format, market)

def __multiple_choice_value(self, market: Market, ret: Any) -> Mapping[int, float]:
def __multiple_choice_value(self, market: Market, account: Account, ret: Any) -> Mapping[int, float]:
if isinstance(ret, Mapping):
ret = {int(val): share for val, share in ret.items()}
elif isinstance(ret, (int, str)):
Expand All @@ -113,11 +116,11 @@ def explain_abstract(self, indent: int = 0, **kwargs: Any) -> str:
"""Explain how the market will resolve and decide to resolve."""
return self._explain_abstract(indent, **kwargs)

def explain_specific(self, market: Market, indent: int = 0, sig_figs: int = 4) -> str:
def explain_specific(self, market: Market, account: Account, indent: int = 0, sig_figs: int = 4) -> str:
"""Explain why the market is resolving the way that it is."""
return self._explain_specific(market, indent, sig_figs)
return self._explain_specific(market, account, indent, sig_figs)

def _explain_specific(self, market: Market, indent: int = 0, sig_figs: int = 4) -> str:
def _explain_specific(self, market: Market, account: Account, indent: int = 0, sig_figs: int = 4) -> str:
f_val = parallel(self._value, market)
warn("Using a default specific explanation. This probably isn't what you want!")
ret = self.explain_abstract(indent=indent).rstrip('\n')
Expand Down Expand Up @@ -156,7 +159,7 @@ def _explain_specific(self, market: Market, indent: int = 0, sig_figs: int = 4)
register_adapter(market.Market, dumps)
register_converter("Market", loads)

VERSION = "0.8.0.1"
VERSION = "0.8.0.9"
__version_info__ = tuple(int(x) for x in VERSION.split('.'))
__all__ = [
"__version_info__", "VERSION", "DoResolveRule", "ResolutionValueRule", "Rule", "Market", "get_client", "rule",
Expand Down
194 changes: 42 additions & 152 deletions ManifoldMarketManager/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,24 @@
from __future__ import annotations

from argparse import ArgumentParser, Namespace
from asyncio import get_event_loop, new_event_loop, set_event_loop
from dataclasses import dataclass
from datetime import datetime, timedelta
from itertools import count
from logging import getLogger
from os import getenv
from pathlib import Path
from sqlite3 import PARSE_COLNAMES, PARSE_DECLTYPES, connect
from time import sleep
from traceback import format_exc
from typing import TYPE_CHECKING, Tuple, cast

from telegram import __version__ as TG_VER

try:
from telegram import __version_info__
except ImportError:
__version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment]

if __version_info__ < (20, 0, 0, "alpha", 1):
raise RuntimeError(
f"This example is not compatible with your current PTB version {TG_VER}. To view the "
f"{TG_VER} version of this example, "
f"visit https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html"
)

from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CallbackQueryHandler

from . import market, require_env
from .consts import AVAILABLE_SCANNERS, EnvironmentVariable, MarketStatus, Response
from .state.persistant import register_db, remove_markets, select_markets, update_market

if TYPE_CHECKING: # pragma: no cover
from sqlite3 import Connection
from typing import Any

from telegram import Update
from telegram.ext import ContextTypes

from . import Market
from .account import Account

logger = getLogger(__name__)

Expand Down Expand Up @@ -375,110 +353,19 @@ def list_command(
return 0


@dataclass
class State:
"""Keeps track of global state for while the Telegram Bot is running."""

application: Application = None # type: ignore
last_response: Response = Response.NO_ACTION
last_text: str = ""


state = State()
keyboard1 = [
[
InlineKeyboardButton("Do Nothing", callback_data=Response.NO_ACTION),
InlineKeyboardButton("Resolve to Default", callback_data=Response.USE_DEFAULT),
],
[InlineKeyboardButton("Cancel Market", callback_data=Response.CANCEL)],
]
keyboard2 = [
[
InlineKeyboardButton("Yes", callback_data="YES"),
InlineKeyboardButton("No", callback_data="NO"),
],
]


@require_env(EnvironmentVariable.DBName)
def register_db() -> Connection:
"""Get a connection to the appropriate database for this bot."""
name = getenv("DBName")
if name is None:
raise EnvironmentError()
do_initialize = not Path(name).exists()
conn = connect(name, detect_types=PARSE_COLNAMES | PARSE_DECLTYPES)
if do_initialize:
conn.execute("CREATE TABLE markets "
"(id INTEGER, market Market, check_rate REAL, last_checked TIMESTAMP);")
conn.commit()
logger.info("Database up and initialized.")
return conn


async def buttons(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Parse the CallbackQuery and update the message text."""
logger.info("Got into the buttons handler")
query = update.callback_query
if query is None or query.data is None:
raise ValueError()

# CallbackQueries need to be answered, even if no notification to the user is needed
# Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery
await query.answer()
logger.info("Got a response from Telegram (%r)", query.data)
if query.data in ("YES", "NO"):
state.last_text += "\n" + query.data
await query.edit_message_text(text=state.last_text)
if query.data != "YES":
logger.info("Was not told yes. Backing out to ask again")
reply_markup = InlineKeyboardMarkup(keyboard1)
await query.edit_message_reply_markup(reply_markup=reply_markup)
else:
logger.info("Confirmation received, shutting dowm Telegram subsystem")
get_event_loop().stop() # lets telegram bot know it can stop
else:
state.last_response = Response(int(query.data))
logger.info("This corresponds to %r", state.last_response)
reply_markup = InlineKeyboardMarkup(keyboard2)
state.last_text += f"\nSelected option: {state.last_response.name}. Are you sure?"
await query.edit_message_text(text=state.last_text)
await query.edit_message_reply_markup(reply_markup=reply_markup)


@require_env(EnvironmentVariable.TelegramAPIKey, EnvironmentVariable.TelegramChatID)
def tg_main(text: str) -> Response:
"""Run the bot."""
async def post_init(self): # type: ignore
reply_markup = InlineKeyboardMarkup(keyboard1)
chat_id = getenv("TelegramChatID")
if chat_id is None:
raise EnvironmentError()
await self.bot.send_message(text=text, reply_markup=reply_markup, chat_id=int(chat_id))

application = Application.builder().token(cast(str, getenv("TelegramAPIKey"))).post_init(post_init).build()
application.add_handler(CallbackQueryHandler(buttons))

state.application = application
state.last_text = text

set_event_loop(new_event_loop())
application.run_polling()
return state.last_response


def watch_reply(conn: Connection, id_: int, mkt: Market, console_only: bool = False) -> None:
def watch_reply(conn: Connection, id_: int, mkt: Market, account: Account, console_only: bool = False) -> None:
"""Watch for a reply from the bot manager in order to check the bot's work."""
text = (f"Hey, we need to resolve {id_} to {mkt.resolve_to()}. It currently has a value of {mkt.current_answer()}."
f"This market is called: {mkt.market.question}\n\n")
text += mkt.explain_abstract()
text = (f"Hey, we need to resolve {id_} to {mkt.resolve_to(account)}. It currently has a value of "
f"{mkt.current_answer(account)}. This market is called: {mkt.market.question}\n\n")
# text += mkt.explain_abstract()
try:
text += "\n\n" + mkt.explain_specific()
text += "\n\n" + mkt.explain_specific(account)
except Exception:
print(format_exc())
logger.exception("Unable to explain a market's resolution automatically")
if not console_only:
response = tg_main(text)
from .confirmation.telegram import tg_main
response = tg_main(text, account)
else:
if input(text + " Use this default value? (y/N) ").lower().startswith("y"):
response = Response.USE_DEFAULT
Expand All @@ -505,40 +392,43 @@ def watch_reply(conn: Connection, id_: int, mkt: Market, console_only: bool = Fa
@require_env(EnvironmentVariable.ManifoldAPIKey, EnvironmentVariable.DBName)
def main(refresh: bool = False, console_only: bool = False) -> int:
"""Go through watched markets and act on rules (resolve, trade, etc)."""
conn = register_db()
mkt: market.Market
for id_, mkt, check_rate, last_checked in conn.execute("SELECT * FROM markets"):
msg = f"Currently checking ID {id_}: {mkt.market.question}"
print(msg)
logger.info(msg)
# print(mkt.explain_abstract())
# print("\n\n" + mkt.explain_specific() + "\n\n")
check = (refresh or not last_checked or (datetime.now() > last_checked + timedelta(hours=check_rate)))
msg = f' - [{"x" if check else " "}] Should I check?'
print(msg)
logger.info(msg)
if check:
check = mkt.should_resolve()
msg = f' - [{"x" if check else " "}] Is elligible to resolve?'
fallback_account = Account.from_env()
with register_db() as conn:
for id_, mkt, check_rate, last_checked, account in select_markets((), db=conn):
if account is None:
account = fallback_account
msg = f"Currently checking ID {id_} for account {account}: {mkt.market.question}"
print(msg)
logger.info(msg)
# print(mkt.explain_abstract())
print("\n\n" + mkt.explain_specific(account) + "\n\n")
check = (refresh or not last_checked or (datetime.now() > last_checked + timedelta(hours=check_rate)))
msg = f' - [{"x" if check else " "}] Should I check?'
print(msg)
logger.info(msg)
if check:
watch_reply(conn, id_, mkt, console_only)

if mkt.market.isResolved:
msg = " - [x] Market resolved, removing from db"
check = mkt.should_resolve(account)
msg = f' - [{"x" if check else " "}] Is elligible to resolve?'
print(msg)
logger.info(msg)
conn.execute(
"DELETE FROM markets WHERE id = ?;",
(id_, )
)
conn.commit()

conn.execute(
"UPDATE markets SET last_checked = ?, market = ? WHERE id = ?;",
(datetime.now(), mkt, id_)
)
conn.commit()
conn.close()
if check:
watch_reply(conn, id_, mkt, account, console_only)

if mkt.market.isResolved:
msg = " - [x] Market resolved, removing from db"
print(msg)
logger.info(msg)
remove_markets(conn, id_)
conn.commit()

update_market(
row_id=id_,
market=mkt,
check_rate=check_rate,
last_checked=datetime.now() if check else last_checked,
account=account,
db=conn,
)
conn.commit()
return 0
4 changes: 3 additions & 1 deletion ManifoldMarketManager/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

import requests_cache

from .consts import EnvironmentVariable

if TYPE_CHECKING: # pragma: no cover
from typing import Any, Callable, Optional

T = TypeVar("T")

CACHE = not getenv("ManifoldMarketManager_NO_CACHE")
CACHE = not getenv(EnvironmentVariable.NO_CACHE)
if CACHE:
requests_cache.install_cache(expire_after=360, allowable_methods=('GET', ))
executor = ThreadPoolExecutor(thread_name_prefix="ManifoldMarketManagerWorker_")
Expand Down
1 change: 1 addition & 0 deletions ManifoldMarketManager/confirmation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Allow users to request confirmation on platforms other than the command line."""
Loading