Skip to content

Commit

Permalink
Render environments with rich tables if using render_mode="human" (#21)
Browse files Browse the repository at this point in the history
For debugging and monitoring, rendering is now being done through a rich table, where the odds for each step/game are displayed in this color legend:
* gray text - no bet was placed on this specific outcome (noop / no operation)
* yellow text - bet was placed on this outcome and on atleast 1 other outcome, and this was the result of the game (warning)
* green text - bet was placed only on this outcome and this was result of the game (success)
* red text - bet was placed on this outcome and this was not the result of the game (failure)
  • Loading branch information
OryJonay authored Jun 19, 2024
1 parent 8347542 commit 6343e12
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 62 deletions.
68 changes: 61 additions & 7 deletions oddsgym/envs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import numexpr
import numpy
from pandas import DataFrame
from rich.live import Live
from rich.table import Table
from tabulate import tabulate

# from infi.traceback import pretty_traceback_and_exit_decorator
Expand Down Expand Up @@ -66,7 +68,7 @@ class BaseOddsEnv(gym.Env):
The starting bank / balance for the environment.
"""

metadata = {"render.modes": ["human"]}
metadata = {"render_modes": [None, "human"]}
HEADERS = [
"Current Step",
"Odds",
Expand All @@ -78,7 +80,9 @@ class BaseOddsEnv(gym.Env):
"Done",
]

def __init__(self, odds, odds_column_names, results=None, starting_bank=300):
def __init__(
self, odds, odds_column_names, results=None, starting_bank=300, render_mode=None
):
"""Initializes a new environment
Parameters
Expand All @@ -90,7 +94,6 @@ def __init__(self, odds, odds_column_names, results=None, starting_bank=300):
results: list of int, default=None
A list of the results, where results[i] is the outcome of odds[i].
"""

super().__init__()
self._odds = odds.copy()
self._results = results
Expand All @@ -103,6 +106,15 @@ def __init__(self, odds, odds_column_names, results=None, starting_bank=300):
self.balance = self.starting_bank = starting_bank
self.current_step = 0
self.bet_size_matrix = numpy.ones(shape=self.observation_space.shape)
self.setup_render_mode(render_mode)

def setup_render_mode(self, render_mode):
self.render_mode = render_mode
if self.render_mode == "human":
self.live = Live(vertical_overflow="visible")

def create_rich_table(self):
self.table = Table(title="Odds Gym", expand=True, *self.HEADERS)

def _get_current_index(self):
return self.current_step % self._odds.shape[0]
Expand Down Expand Up @@ -181,15 +193,19 @@ def step(self, action):
info.update(legal_bet=True)
else:
reward = -(bet * self.bet_size_matrix).sum()
info.update(results=results.argmax())
info.update(results=results)
info.update(reward=reward)
self.last_info = info
self.render()
self.current_step += 1
if self.finish():
done = True
odds = numpy.ones(shape=self.observation_space.shape)
else:
odds = self.get_odds()
info.update(done=done)
if done and self.render_mode == "human":
self.live.stop()
return odds, reward, done, truncated, info

def get_reward(self, bet, odds, results):
Expand Down Expand Up @@ -222,17 +238,31 @@ def reset(self, *, seed=None, options=None):
"""
self.balance = self.starting_bank
self.current_step = 0
self.last_info = {}
if self.render_mode == "human":
if self.live.is_started:
self.live.stop()
self.live.start()
self.create_rich_table()
return self.get_odds(), {}

def render(self, mode="human"):
def render(self):
"""Outputs the current balance and the current step.
Returns
-------
msg : str
A string with the current balance and the current step.
"""
print("Current balance at step {}: {}".format(self.current_step, self.balance))
if self.render_mode is None:
return
if self.render_mode == "human":
info = self.last_info
info.update(odds=self.pretty_odds(info))
self.table.add_row(
*[str(info[key.lower().replace(" ", "_")]) for key in self.HEADERS]
)
self.live.update(self.table)

def finish(self):
"""Checks if the episode has reached an end.
Expand Down Expand Up @@ -301,9 +331,10 @@ def create_info(self, action):
"""
return {
"current_step": self.current_step,
"odds": self.get_odds().tolist(),
"odds": self.get_odds(),
"verbose_action": self._verbose_actions[action],
"action": action,
"bet": self.get_bet(action),
"balance": self.balance,
"reward": 0,
"legal_bet": False,
Expand All @@ -315,6 +346,29 @@ def pretty_print_info(self, info):
values = [info[key.replace(" ", "_").lower()] for key in self.HEADERS]
print("\n" + tabulate([values], headers=self.HEADERS, tablefmt="orgtbl"))

def pretty_odds(self, info):
odds = info["odds"]
final_odds = ""
noop_color = "[gray]"
failure_color = "[red]"
for i, row in enumerate(odds):
if row.any():
if numpy.count_nonzero(info["bet"][i]) == 1:
success_color = "[green]"
else:
success_color = "[yellow]"
for j, value in enumerate(row):
value_color = noop_color
if info["bet"][i][j]:
value_color = (
success_color
if info["bet"][i][j] == info["results"][i][j]
else failure_color
)
final_odds += f"{value_color}{value}[/] "
final_odds += "\n"
return final_odds

def _rescale_form(self, form):
if form == 1:
form -= numpy.finfo(numpy.float64).eps
Expand Down
7 changes: 2 additions & 5 deletions oddsgym/envs/daily_bets.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,8 @@ class DailyOddsEnv(BaseOddsEnv):
"Date",
"Current Step",
"Odds",
"Verbose Action",
"Action",
"Balance",
"Reward",
"Results",
"Done",
]

def __init__(
Expand Down Expand Up @@ -281,13 +277,14 @@ def create_info(self, action):
The info dictionary.
"""
return {
"date": self.days[self.current_step],
"date": self.days[self.current_step].strftime("%Y-%m-%d"),
"current_step": self.current_step,
"odds": self.get_odds(),
"verbose_action": [
self._verbose_actions[act] for act in numpy.floor(action).astype(int)
],
"action": action,
"bet": self.get_bet(action),
"balance": self.balance,
"reward": 0,
"legal_bet": False,
Expand Down
2 changes: 1 addition & 1 deletion oddsgym/envs/footballdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def __init__(self, config=None, *args, **kwargs):
inplace=True,
)
super().__init__(
odds_dataframe[self.ENV_COLUMNS + self.ODDS_COLUMNS], **env_config
odds_dataframe[self.ENV_COLUMNS + self.ODDS_COLUMNS], **env_config, **kwargs
)
self._extra_odds = odds_dataframe
self._extra = extra
Expand Down
20 changes: 7 additions & 13 deletions oddsgym/envs/soccer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,22 @@ def __init__(self, soccer_bets_dataframe, *args, **kwargs):
odds = odds.values
results = results.values
self.teams = soccer_bets_dataframe[["home_team", "away_team"]]
self.HEADERS.insert(2, "Teams")
super().__init__(odds, self.odds_column_names, results, *args, **kwargs)

def render(self, mode="human"):
"""Outputs the current team names, balance and step.
Returns
-------
msg : str
A string with the current team names, balance and step.
"""
def create_info(self, action):
info = super().create_info(action)
index = self._get_current_index()
teams = self.teams.iloc[index]
teams = teams.itertuples() if isinstance(teams, pandas.DataFrame) else [teams]
teams_str = ", ".join(
teams_str = "\n".join(
[
"Home Team {} VS Away Team {}".format(row.home_team, row.away_team)
f"[blue]{row.home_team}[/] VS [magenta]{row.away_team}[/]"
for row in teams
]
)
teams_str = teams_str + "."
print(teams_str)
super().render(mode)
info.update(teams=teams_str)
return info


class ThreeWaySoccerPercentageOddsEnv(ThreeWaySoccerOddsEnv):
Expand Down
23 changes: 10 additions & 13 deletions oddsgym/envs/tennis.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,24 @@ def __init__(self, tennis_bets_dataframe, *args, **kwargs):
odds = odds.values
results = results.values
self.players = tennis_bets_dataframe[["winner", "loser"]]
self.HEADERS.insert(2, "Players")
super().__init__(odds, self.odds_column_names, results, *args, **kwargs)

def render(self, mode="human"):
"""Outputs the current player names, balance and step.
Returns
-------
msg : str
A string with the current player names, balance and step.
"""
def create_info(self, action):
info = super().create_info(action)
index = self._get_current_index()
players = self.players.iloc[index]
players = (
players.itertuples() if isinstance(players, pandas.DataFrame) else [players]
)
players_str = ", ".join(
["Player {} VS Player {}".format(row.winner, row.loser) for row in players]
players_str = "\n".join(
[
"[blue]{}[/] VS [magenta]{}[/]".format(row.winner, row.loser)
for row in players
]
)
players_str = players_str + "."
print(players_str)
super().render(mode)
info.update(players=players_str)
return info


class TennisPercentageOddsEnv(TennisOddsEnv):
Expand Down
10 changes: 5 additions & 5 deletions tests/test_base_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def test_reset(basic_env):
assert not done
assert basic_env.current_step == 1
assert info["legal_bet"]
assert info["results"] == 1
assert numpy.array_equal(info["results"], numpy.array([[0.0, 1.0]]))
assert info["reward"] == 1
assert not info["done"]
odds, reward, done, *_ = basic_env.step(2)
Expand All @@ -59,13 +59,13 @@ def test_info(basic_env):
assert not info["legal_bet"]
assert info["results"] is None
assert not info["done"]
basic_env.pretty_print_info(info)
# basic_env.pretty_print_info(info)


def test_render(basic_env):
def test_render_render_mode_none(basic_env):
with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout:
basic_env.render()
assert fake_stdout.getvalue() == "Current balance at step 0: 10\n"
assert basic_env.render() is None
assert fake_stdout.getvalue() == ""


@pytest.mark.parametrize("action", range(4))
Expand Down
2 changes: 1 addition & 1 deletion tests/test_daily_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,4 @@ def test_info(daily_bets_env):
assert not info["legal_bet"]
assert info["results"] is None
assert not info["done"]
daily_bets_env.pretty_print_info(info)
# daily_bets_env.pretty_print_info(info)
6 changes: 1 addition & 5 deletions tests/test_tennis_daily_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,4 @@ def test_multiple_steps(tennis_daily_env):
def test_render(tennis_daily_env):
with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout:
tennis_daily_env.render()
assert fake_stdout.getvalue() == (
"Player Berrettini M. VS Player Harris A., "
"Player Berankis R. VS Player Carballes Baena R.."
"\nCurrent balance at step 0: 10\n"
)
assert fake_stdout.getvalue() == ""
5 changes: 1 addition & 4 deletions tests/test_tennis_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,4 @@ def test_multiple_steps(tennis_env):
def test_render(tennis_env):
with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout:
tennis_env.render()
assert (
fake_stdout.getvalue()
== "Player Berrettini M. VS Player Harris A..\nCurrent balance at step 0: 10\n"
)
assert fake_stdout.getvalue() == ""
5 changes: 1 addition & 4 deletions tests/test_three_way_daily_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,4 @@ def test_multiple_steps_non_uniform(three_way_daily_env_non_uniform):
def test_render(three_way_daily_env_non_uniform):
with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout:
three_way_daily_env_non_uniform.render()
assert (
fake_stdout.getvalue() == "Home Team FCB VS Away Team PSG, "
"Home Team MCB VS Away Team MTA.\nCurrent balance at step 0: 10\n"
)
assert fake_stdout.getvalue() == ""
5 changes: 1 addition & 4 deletions tests/test_three_way_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,4 @@ def test_multiple_steps(three_way_env):
def test_render(three_way_env):
with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout:
three_way_env.render()
assert (
fake_stdout.getvalue()
== "Home Team FCB VS Away Team PSG.\nCurrent balance at step 0: 10\n"
)
assert fake_stdout.getvalue() == ""

0 comments on commit 6343e12

Please sign in to comment.