ManifoldMarketManager/ManifoldMarketManager/application.py

435 lines
16 KiB
Python

"""Contains functions which are needed to run the runner script, but nowhere else."""
from __future__ import annotations
from argparse import ArgumentParser, Namespace
from datetime import datetime, timedelta
from itertools import count
from logging import getLogger
from os import getenv
from time import sleep
from traceback import format_exc
from typing import TYPE_CHECKING, Tuple, cast
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 . import Market
from .account import Account
logger = getLogger(__name__)
def parse_args(*args: Any, **kwargs: Any) -> Namespace:
"""Parse arguments for the CLI."""
main_parser = ArgumentParser()
main_parser.add_argument('--no-logging', action='store_false', dest='logging', default=True)
main_parser.add_argument('-v', '--verbose', action='count', default=0)
main_parser.add_argument('--just-parse', action='store_true', default=False)
subparsers = main_parser.add_subparsers()
import_parser = subparsers.add_parser('import')
import_parser.add_argument('account', action='store', type=str)
import_parser.add_argument('file', action='store', type=str, nargs='?')
import_parser.add_argument('--interactive', action='store_true')
group = import_parser.add_mutually_exclusive_group(required=False)
group.add_argument('--yaml', action='store_true')
group.add_argument('--json', action='store_true')
group.add_argument('--repl', action='store_true')
import_parser.set_defaults(func=import_command)
# TODO: add templates here
quick_import_parser = subparsers.add_parser('quick-import')
quick_import_parser.add_argument('account', action='store', type=str)
quick_import_parser.add_argument(
'--resolve-when', nargs=2, action='append',
help="Should be a qualified rule name, followed by a JSON string of its initializers"
)
quick_import_parser.add_argument(
'--resolve-to', nargs=2, action='append', required=True,
help="Should be a qualified rule name, followed by a JSON string of its initializers"
)
quick_import_parser.add_argument('-n', '--notes', type=str, action='store', default='')
group = quick_import_parser.add_mutually_exclusive_group(required=True)
group.add_argument('-u', '--url', action='store', type=str)
group.add_argument('-s', '--slug', action='store', type=str)
group.add_argument('-i', '--id', dest='id_', action='store', type=str)
quick_import_parser.add_argument('-c', '--check-rate', action='store', dest='rate', help='Check rate in hours')
quick_import_parser.add_argument('-rnd', '--round', dest='round_', action='store_true')
quick_import_parser.add_argument('-cur', '--current', action='store_true')
quick_import_parser.add_argument(
'-rd', '--rel-date', action='store', dest='rel_date',
help='Please give as "year/month/day" or "year-month-day". Used in: poll, git PR'
)
quick_import_parser.add_argument(
'-pr', '--pull-request', action='store', dest='pr_slug', help='Please give as "owner/repo/num"'
)
quick_import_parser.add_argument('-pb', '--pull-binary', action='store_true', dest='pr_bin')
quick_import_parser.add_argument('-rs', '--random-seed', action='store')
quick_import_parser.add_argument('-rr', '--random-rounds', action='store', type=int, default=1)
quick_import_parser.add_argument('-ri', '--random-index', action='store_true')
quick_import_parser.add_argument('-is', '--index-size', action='store', type=int)
quick_import_parser.set_defaults(func=quick_create_command)
# must finish import_parser first
create_parser = subparsers.add_parser('create', parents=[import_parser], add_help=False)
create_parser.add_argument('--queue-if-no-funds', action='store_true')
create_parser.add_argument('--queue', action='store_true')
create_parser.set_defaults(func=create_command)
quick_create_parser = subparsers.add_parser('quick-create')
quick_create_parser.add_argument(
'type', type=str, choices=["BINARY", "PSEUDO_NUMERIC", "FREE_RESPONSE", "MULTIPLE_CHOICE"]
)
quick_create_parser.add_argument('account', action='store', type=str)
quick_create_parser.add_argument('close-on', action='store', type=str)
quick_create_parser.add_argument(
'--resolve-when', nargs=2, action='append',
help="Should be a qualified rule name, followed by a JSON string of its initializers"
)
quick_create_parser.add_argument(
'--resolve-to', nargs=2, action='append', required=True,
help="Should be a qualified rule name, followed by a JSON string of its initializers"
)
quick_create_parser.add_argument('-n', '--notes', type=str, action='store', default='')
quick_create_parser.set_defaults(func=quick_create_command)
scan_parser = subparsers.add_parser('scan')
scan_parser.add_argument('--disable-all', action='store_false', dest='all_scanners', default=True)
for scanner in AVAILABLE_SCANNERS:
scan_parser.add_argument(
f'--enable-{scanner.replace(".", "-")}', dest='scanners', action='append_const', const=scanner
)
scan_parser.set_defaults(func=scan_command)
run_parser = subparsers.add_parser('run')
run_parser.add_argument('--enable-all-scanners', action='store_true', dest='all_scanners', default=False)
for scanner in AVAILABLE_SCANNERS:
run_parser.add_argument(
f'--enable-{scanner.replace(".", "-")}', dest='scanners', action='append_const', const=scanner
)
run_parser.add_argument(
'-r', '--refresh', action='store_true',
help="Ignore time last checked and look at all markets immediately"
)
run_parser.add_argument('-c', '--console-only', action='store_true')
run_parser.set_defaults(func=run_command)
loop_parser = subparsers.add_parser('loop', parents=[run_parser], add_help=False)
loop_parser.add_argument(
'-p', '--period', action='store', type=float, help='how long to wait between loops, in minutes'
)
loop_parser.add_argument(
'-t', '--times', action='store', type=float, default=float('inf'),
help='how many times to loop (default infinity)'
)
loop_parser.set_defaults(func=loop_command)
edit_parser = subparsers.add_parser('edit')
edit_parser.add_argument('ids', nargs='+', type=int)
edit_parser.set_defaults(func=edit_command)
remove_parser = subparsers.add_parser('remove')
remove_parser.add_argument('ids', nargs='+', type=int)
remove_parser.add_argument('--assume-yes', '-y', action='store_true')
remove_parser.set_defaults(func=remove_command)
list_parser = subparsers.add_parser('list')
list_parser.add_argument('--stats', action='store_true')
list_parser.add_argument('--sig-figs', action='store', type=int, default=4)
list_parser.set_defaults(func=list_command)
parsed: Namespace = main_parser.parse_args(*args, **kwargs)
if hasattr(parsed, 'all_scanners') and parsed.all_scanners:
parsed.scanners = AVAILABLE_SCANNERS
return parsed
def _print_uncaught_args(kwargs: dict[str, Any]) -> None:
if getenv("DEBUG") and kwargs:
print("Unrecognized arguments:")
print("\n".join(f'{key}: {value}' for key, value in kwargs.items()))
def import_command(**kwargs: Any) -> int:
"""Import markets from a file without creating any."""
_print_uncaught_args(kwargs)
return -1
def quick_import_command(
url: str | None = None,
slug: str | None = None,
id_: str | None = None,
rel_date: str | None = None,
random_index: bool = False,
random_seed: bool = False,
random_rounds: int = 1,
round_: bool = False,
current: bool = False,
index_size: int | None = None,
pr_slug: str | None = None,
pr_bin: bool = False,
**kwargs: Any
) -> int:
"""Import a single market using the old-style arguments."""
_print_uncaught_args(kwargs)
if url:
mkt = Market.from_url(url)
elif slug:
mkt = Market.from_slug(slug)
else:
mkt = Market.from_id(cast(str, id_))
if rel_date:
sections = rel_date.split('/')
if len(sections) == 1:
sections = rel_date.split('-')
try:
date: None | tuple[int, int, int] = tuple(int(x) for x in sections) # type: ignore[assignment]
except ValueError:
raise
else:
date = None
if random_index:
from .rule.generic import ResolveRandomIndex
mkt.resolve_to_rules.append(
ResolveRandomIndex(random_seed, size=index_size, rounds=random_rounds)
)
if round_:
from .rule.manifold.this import RoundValueRule
mkt.resolve_to_rules.append(RoundValueRule()) # type: ignore
if current:
from .rule.manifold.this import CurrentValueRule
mkt.resolve_to_rules.append(CurrentValueRule())
if pr_slug:
from .rule.github import ResolveToPR, ResolveToPRDelta, ResolveWithPR
pr_: list[str | int] = list(pr_slug.split('/'))
pr_[-1] = int(pr_[-1])
pr = cast(Tuple[str, str, int], tuple(pr_))
mkt.do_resolve_rules.append(ResolveWithPR(*pr))
if date:
mkt.resolve_to_rules.append(ResolveToPRDelta(*pr, datetime(*date)))
elif pr_bin:
mkt.resolve_to_rules.append(ResolveToPR(*pr))
else:
raise ValueError("No resolve rule provided")
if not mkt.do_resolve_rules:
if not date:
from .rule.manifold.this import ThisMarketClosed
mkt.do_resolve_rules.append(ThisMarketClosed())
else:
from .rule.generic import ResolveAtTime
mkt.do_resolve_rules.append(ResolveAtTime(datetime(*date)))
with register_db() as conn:
idx = max(((0, ), *conn.execute("SELECT id FROM markets;")))[0] + 1
conn.execute("INSERT INTO markets values (?, ?, ?, ?);", (idx, mkt, 1, None))
conn.commit()
msg = f"Successfully added as ID {idx}!"
print(msg)
logger.info(msg)
return 0
def create_command(**kwargs: Any) -> int:
"""Create markets from a file, then import them."""
_print_uncaught_args(kwargs)
return -1
def quick_create_command(**kwargs: Any) -> int:
"""Quickly create a single market without need for a file, then import it."""
_print_uncaught_args(kwargs)
return -1
def scan_command(**kwargs: Any) -> int:
"""Scan services for markets to create."""
_print_uncaught_args(kwargs)
return -1
def run_command(
refresh: bool = False,
console_only: bool = False,
scanners: list[str] = None, # type: ignore[assignment]
**kwargs: Any
) -> int:
"""Go through our markets and take actions if needed."""
_print_uncaught_args(kwargs)
return main(refresh, console_only) or 0
def loop_command(
period: float = 5,
times: float = 5,
**kwargs: Any
) -> int:
"""Run this service multiple times."""
# TODO: turn this into an event queue instead
for i in count():
if i > times:
break
run_command(**kwargs)
sleep(period * 60)
return 0
def edit_command(**kwargs: Any) -> int:
"""Edit a market from a temporary file or repl."""
_print_uncaught_args(kwargs)
return -1
def remove_command(
ids: list[int],
assume_yes: bool = False,
**kwargs: Any
) -> int:
"""Remove markets from the database."""
_print_uncaught_args(kwargs)
for id_ in ids:
with register_db() as conn:
try:
((mkt, ), ) = conn.execute(
"SELECT market FROM markets WHERE id = ?;",
(id_, )
)
except ValueError:
print(f"No market with id {id_} exists.")
return 1
question = f'Are you sure you want to remove {id_}: "{mkt.market.question}" (y/N)?'
if (assume_yes or input(question).lower().startswith('y')):
conn.execute(
"DELETE FROM markets WHERE id = ?;",
(id_, )
)
conn.commit()
logger.info(f"{id_} removed from db")
return 0
def list_command(
stats: bool = False,
verbose: int = 0,
sig_figs: int = 4,
**kwargs: Any
) -> int:
"""List markets from the database in varying verbosity."""
_print_uncaught_args(kwargs)
with register_db() as conn:
id_: int
mkt: Market
check_rate: float
last_check: datetime | None
for id_, mkt, check_rate, last_check in conn.execute("SELECT * FROM markets"):
info = f"Market ID: {id_} (internal), {mkt.market.id} (manifold)\n"
hours = int(check_rate)
minutes = (check_rate - hours) // 60
seconds = ((check_rate - hours) / 60 - minutes) // 60
info += f"Checks every {hours}:{minutes}:{seconds}\tLast checked: {last_check}\n"
info += f"Question: {mkt.market.question}\n"
if verbose:
info += mkt.explain_abstract(sig_figs=sig_figs) + "\n"
print(info)
return 0
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(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(account)
except Exception:
print(format_exc())
logger.exception("Unable to explain a market's resolution automatically")
if not console_only:
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
elif input("Cancel the market? (y/N) ").lower().startswith("y"):
response = Response.CANCEL
else:
response = Response.NO_ACTION
if response == Response.NO_ACTION:
return
elif response == Response.USE_DEFAULT:
resp = mkt.resolve()
elif response == Response.CANCEL:
resp = mkt.cancel()
if mkt.status != MarketStatus.RESOLVED:
raise RuntimeError(resp)
conn.execute(
"DELETE FROM markets WHERE id = ?;",
(id_, )
)
conn.commit()
@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)."""
mkt: market.Market
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:
check = mkt.should_resolve(account)
msg = f' - [{"x" if check else " "}] Is elligible to resolve?'
print(msg)
logger.info(msg)
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