Skip to content

Commit

Permalink
Merge pull request #215 from BenBrostoff/min-matchups-setting
Browse files Browse the repository at this point in the history
Min matchups setting
  • Loading branch information
BenBrostoff authored Dec 27, 2023
2 parents c5f774b + 54078d6 commit 23c7d31
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 68 deletions.
31 changes: 31 additions & 0 deletions draftfast/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(
self.locked_for_exposure = exposure_dict.get("locked", [])
self.custom_rules = settings.custom_rules
self.min_teams = rule_set.min_teams or settings.min_teams
self.min_matchups = rule_set.min_matchups or settings.min_matchups
self.position_per_team_rules = rule_set.position_per_team_rules

self.player_to_idx_map = {}
Expand Down Expand Up @@ -68,6 +69,8 @@ def __init__(
raise PlayerBanAndLockException(player.name)

self.teams = set([p.team for p in self.players])
if self.min_matchups:
self.matchups = set([p.matchup for p in self.players])
self.objective = self.solver.Objective()
self.objective.SetMaximization()

Expand Down Expand Up @@ -110,6 +113,7 @@ def solve(self) -> bool:
self._set_combo()
self._set_no_duplicate_lineups()
self._set_min_teams()
self._set_min_matchups()
self._set_custom_rules()
self._set_position_team_constraints()

Expand Down Expand Up @@ -372,6 +376,10 @@ def _set_no_duplicate_lineups(self):
repeated_players.SetCoefficient(self.variables[i], 1)

def _set_min_teams(self):
"""
Add constraints for maximum players on an individual team
and total represented teams if applicable
"""
teams = []
min_teams = self.min_teams

Expand All @@ -385,6 +393,10 @@ def _set_min_teams(self):
for i, p in self.enumerated_players
if p.team == team
]

# Teams in lineup must be <= total teams
# TODO - determine if this is actually necessary,
# as team is always 1:1 with player
self.solver.Add(
team_var <= self.solver.Sum(players_on_team)
)
Expand All @@ -393,5 +405,24 @@ def _set_min_teams(self):
>= self.solver.Sum(players_on_team)
)

# If min matchups is more than or equal to min_teams,
# this constraint is redundant
# Ex given min matchups of two, there will always be two teams,
# so adding this constraint is needless if data is good.
# That said, keep constraint to spot check data.
if len(teams) > 0:
self.solver.Add(self.solver.Sum(teams) >= self.min_teams)

def _set_min_matchups(self):
"""
Add minimum required matchups in a lineup,
generally two for classic sports
"""
matchups = []
if self.min_matchups and self.min_matchups > 1:
for matchup in self.matchups:
if matchup:
matchup_var = self.solver.IntVar(0, 1, matchup)
matchups.append(matchup_var)

self.solver.Add(self.solver.Sum(matchups) >= self.min_matchups)
12 changes: 7 additions & 5 deletions draftfast/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
defensive_positions=None,
max_players_per_team=None,
min_teams=None,
min_matchups=None,
position_per_team_rules=None,
game_type="classic",
):
Expand All @@ -41,6 +42,7 @@ def __init__(
self.max_players_per_team = max_players_per_team or (roster_size - 1)
self.position_per_team_rules = position_per_team_rules
self.min_teams = min_teams
self.min_matchups = min_matchups


DK_NBA_RULE_SET = RuleSet(
Expand All @@ -50,7 +52,7 @@ def __init__(
salary_max=SALARY_CAP_BY_SITE_BY_LEAGUE[DRAFT_KINGS]["NBA"],
position_limits=POSITIONS_BY_SITE_BY_LEAGUE[DRAFT_KINGS]["NBA"],
general_position_limits=NBA_GENERAL_POSITIONS,
min_teams=3,
min_matchups=2,
)

DK_NBA_SHOWDOWN_RULE_SET = RuleSet(
Expand Down Expand Up @@ -79,7 +81,7 @@ def __init__(
salary_max=SALARY_CAP_BY_SITE_BY_LEAGUE[DRAFT_KINGS]["WNBA"],
position_limits=POSITIONS_BY_SITE_BY_LEAGUE[DRAFT_KINGS]["WNBA"],
general_position_limits=NBA_GENERAL_POSITIONS,
min_teams=3,
min_matchups=2,
)

FD_WNBA_RULE_SET = RuleSet(
Expand All @@ -100,7 +102,7 @@ def __init__(
offensive_positions=["QB", "RB", "WR", "TE"],
defensive_positions=["DST"],
general_position_limits=[],
min_teams=3,
min_matchups=2,
)

FD_NFL_RULE_SET = RuleSet(
Expand Down Expand Up @@ -215,7 +217,7 @@ def __init__(
salary_max=SALARY_CAP_BY_SITE_BY_LEAGUE[DRAFT_KINGS]["MLB"],
position_limits=POSITIONS_BY_SITE_BY_LEAGUE[DRAFT_KINGS]["MLB"],
general_position_limits=MLB_GENERAL_POSITIONS,
min_teams=3,
min_matchups=2,
position_per_team_rules=[[lambda pos: "P" not in pos, 5]],
)

Expand Down Expand Up @@ -256,7 +258,7 @@ def __init__(
position_limits=POSITIONS_BY_SITE_BY_LEAGUE[DRAFT_KINGS]["NHL"],
offensive_positions=["C", "W"],
defensive_positions=["G", "D"],
min_teams=3,
min_matchups=2,
general_position_limits=[],
)

Expand Down
16 changes: 16 additions & 0 deletions draftfast/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,23 @@ def __init__(
no_defense_against_captain=False,
showdown_teams=None,
min_teams=2,
min_matchups=None,
custom_rules=None,
):
"""
A note on defaults:
min_teams 2 - this constraint is common across Classic and Showdown.
- In Showdown, you must have two teams represented, although only
one matchup exists to choose from.
- In Classic, the constraint of two matchups
forces a minimum of two teams
Certain sports additionally impose
max players per team constraints, which
is not overridable outside of mutating RuleSets.
"""
self.stacks = stacks
self.existing_rosters = existing_rosters or []
self.force_combo = force_combo
Expand All @@ -118,6 +133,7 @@ def __init__(
self.no_defense_against_captain = no_defense_against_captain
self.showdown_teams = showdown_teams
self.min_teams = min_teams
self.min_matchups = min_matchups
self.custom_rules = custom_rules

# TODO: format this like a proper repr(), i.e. <OptimizerSettings: ...>
Expand Down
26 changes: 13 additions & 13 deletions draftfast/test/test_custom_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@

def construct_pool():
mock_nba_pool = [
Player(name="A1", cost=5500, proj=100, pos="PG"),
Player(name="A2", cost=5500, proj=41, pos="PG"),
Player(name="A100", cost=5500, proj=501, pos="PG"),
Player(name="A101", cost=5500, proj=500, pos="PG"),
Player(name="A11", cost=5500, proj=50, pos="PG"),
Player(name="A3", cost=5500, proj=42, pos="SG"),
Player(name="A4", cost=5500, proj=0, pos="SG"),
Player(name="A5", cost=5500, proj=44, pos="SF"),
Player(name="A6", cost=5500, proj=45, pos="SF"),
Player(name="A7", cost=5500, proj=46, pos="PF"),
Player(name="A8", cost=5500, proj=47, pos="PF"),
Player(name="A9", cost=5500, proj=0, pos="C"),
Player(name="A10", cost=5500, proj=49, pos="C"),
Player(name="A1", cost=5500, proj=100, pos="PG", matchup="AvB"),
Player(name="A2", cost=5500, proj=41, pos="PG", matchup="AvB"),
Player(name="A100", cost=5500, proj=501, pos="PG", matchup="AvB"),
Player(name="A101", cost=5500, proj=500, pos="PG", matchup="AvB"),
Player(name="A11", cost=5500, proj=50, pos="PG", matchup="AvB"),
Player(name="A3", cost=5500, proj=42, pos="SG", matchup="AvB"),
Player(name="A4", cost=5500, proj=0, pos="SG", matchup="CvD"),
Player(name="A5", cost=5500, proj=44, pos="SF", matchup="CvD"),
Player(name="A6", cost=5500, proj=45, pos="SF", matchup="CvD"),
Player(name="A7", cost=5500, proj=46, pos="PF", matchup="CvD"),
Player(name="A8", cost=5500, proj=47, pos="PF", matchup="CvD"),
Player(name="A9", cost=5500, proj=0, pos="C", matchup="CvD"),
Player(name="A10", cost=5500, proj=49, pos="C", matchup="CvD"),
]

team = 0
Expand Down
22 changes: 11 additions & 11 deletions draftfast/test/test_mlb.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ def test_mlb_dk():

def test_five_batters_max():
player_pool = [
Player(pos="P", name="A", cost=5000, team="C"),
Player(pos="P", name="B", cost=5000, team="B"),
Player(pos="1B", name="C", cost=5000, team="C"),
Player(pos="OF", name="H", cost=5000, team="C"),
Player(pos="OF", name="I", cost=5000, team="C"),
Player(pos="C", name="F", cost=5000, team="C"),
Player(pos="2B", name="D", cost=5000, team="C"),
Player(pos="2B", name="E", cost=5000, team="C"),
Player(pos="3B", name="E", cost=5000, team="C"),
Player(pos="SS", name="G", cost=5000, team="Q"),
Player(pos="OF", name="J", cost=5000, team="G"),
Player(pos="P", name="A", cost=5000, team="C", matchup="CvB"),
Player(pos="P", name="B", cost=5000, team="B", matchup="CvB"),
Player(pos="1B", name="C", cost=5000, team="C", matchup="CvB"),
Player(pos="OF", name="H", cost=5000, team="C", matchup="CvB"),
Player(pos="OF", name="I", cost=5000, team="C", matchup="CvB"),
Player(pos="C", name="F", cost=5000, team="C", matchup="CvB"),
Player(pos="2B", name="D", cost=5000, team="C", matchup="CvB"),
Player(pos="2B", name="E", cost=5000, team="C", matchup="CvB"),
Player(pos="3B", name="E", cost=5000, team="C", matchup="CvB"),
Player(pos="SS", name="G", cost=5000, team="Q", matchup="QvG"),
Player(pos="OF", name="J", cost=5000, team="G", matchup="QvG"),
]

roster = run(
Expand Down
86 changes: 48 additions & 38 deletions draftfast/test/test_optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,33 @@


mock_nba_pool = [
Player(name="A1", cost=5500, proj=40, pos="PG"),
Player(name="A2", cost=5500, proj=41, pos="PG"),
Player(name="A11", cost=5500, proj=50, pos="PG"),
Player(name="A3", cost=5500, proj=42, pos="SG"),
Player(name="A4", cost=5500, proj=43, pos="SG"),
Player(name="A5", cost=5500, proj=44, pos="SF"),
Player(name="A6", cost=5500, proj=45, pos="SF"),
Player(name="A7", cost=5500, proj=46, pos="PF"),
Player(name="A8", cost=5500, proj=47, pos="PF"),
Player(name="A9", cost=5500, proj=48, pos="C"),
Player(name="A10", cost=5500, proj=49, pos="C"),
Player(name="A1", cost=5500, proj=40, pos="PG", matchup="AvB"),
Player(name="A2", cost=5500, proj=41, pos="PG", matchup="AvB"),
Player(name="A11", cost=5500, proj=50, pos="PG", matchup="AvB"),
Player(name="A3", cost=5500, proj=42, pos="SG", matchup="AvB"),
Player(name="A4", cost=5500, proj=43, pos="SG", matchup="AvB"),
Player(name="A5", cost=5500, proj=44, pos="SF", matchup="AvB"),
Player(name="A6", cost=5500, proj=45, pos="SF", matchup="AvB"),
Player(name="A7", cost=5500, proj=46, pos="PF", matchup="AvB"),
Player(name="A8", cost=5500, proj=47, pos="PF", matchup="AvB"),
Player(name="A9", cost=5500, proj=48, pos="C", matchup="CvD"),
Player(name="A10", cost=5500, proj=49, pos="C", matchup="CvD"),
]

mock_nfl_pool = [
Player(name="A1", cost=5500, proj=40, pos="QB"),
Player(name="A2", cost=5500, proj=41, pos="QB"),
Player(name="A11", cost=5500, proj=50, pos="WR"),
Player(name="A3", cost=5500, proj=42, pos="WR"),
Player(name="A4", cost=5500, proj=43, pos="WR"),
Player(name="A5", cost=5500, proj=44, pos="WR"),
Player(name="A6", cost=5500, proj=45, pos="RB"),
Player(name="A7", cost=5500, proj=46, pos="RB"),
Player(name="A8", cost=5500, proj=47, pos="RB"),
Player(name="A9", cost=5500, proj=48, pos="TE"),
Player(name="A10", cost=5500, proj=49, pos="TE"),
Player(name="A12", cost=5500, proj=51, pos="DST"),
Player(name="A13", cost=5500, proj=52, pos="DST"),
Player(name="A1", cost=5500, proj=40, pos="QB", matchup="AvB"),
Player(name="A2", cost=5500, proj=41, pos="QB", matchup="AvB"),
Player(name="A11", cost=5500, proj=50, pos="WR", matchup="AvB"),
Player(name="A3", cost=5500, proj=42, pos="WR", matchup="AvB"),
Player(name="A4", cost=5500, proj=43, pos="WR", matchup="AvB"),
Player(name="A5", cost=5500, proj=44, pos="WR", matchup="AvB"),
Player(name="A6", cost=5500, proj=45, pos="RB", matchup="AvB"),
Player(name="A7", cost=5500, proj=46, pos="RB", matchup="CvD"),
Player(name="A8", cost=5500, proj=47, pos="RB", matchup="CvD"),
Player(name="A9", cost=5500, proj=48, pos="TE", matchup="CvD"),
Player(name="A10", cost=5500, proj=49, pos="TE", matchup="CvD"),
Player(name="A12", cost=5500, proj=51, pos="DST", matchup="CvD"),
Player(name="A13", cost=5500, proj=52, pos="DST", matchup="CvD"),
]


Expand Down Expand Up @@ -554,6 +554,7 @@ def test_multi_position_group_constraint():
pos="QB",
possible_positions="QB/WR",
multi_position=True,
matchup="AvB",
),
Player(
name="A",
Expand All @@ -562,15 +563,17 @@ def test_multi_position_group_constraint():
pos="WR",
possible_positions="QB/WR",
multi_position=True,
matchup="CvD",
),
Player(name="B", cost=5500, proj=41, pos="QB"),
Player(name="B", cost=5500, proj=41, pos="QB", matchup="AvB"),
Player(
name="C",
cost=5500,
proj=500,
pos="WR",
possible_positions="RB/WR",
multi_position=True,
matchup="AvB",
),
Player(
name="C",
Expand All @@ -579,20 +582,22 @@ def test_multi_position_group_constraint():
pos="RB",
possible_positions="RB/WR",
multi_position=True,
),
Player(name="D", cost=5500, proj=42, pos="WR"),
Player(name="E", cost=5500, proj=43, pos="WR"),
Player(name="F", cost=5500, proj=44, pos="WR"),
Player(name="G", cost=5500, proj=45, pos="RB"),
Player(name="H", cost=5500, proj=46, pos="RB"),
Player(name="I", cost=5500, proj=47, pos="RB"),
matchup="AvB",
),
Player(name="D", cost=5500, proj=42, pos="WR", matchup="AvB"),
Player(name="E", cost=5500, proj=43, pos="WR", matchup="AvB"),
Player(name="F", cost=5500, proj=44, pos="WR", matchup="AvB"),
Player(name="G", cost=5500, proj=45, pos="RB", matchup="AvB"),
Player(name="H", cost=5500, proj=46, pos="RB", matchup="AvB"),
Player(name="I", cost=5500, proj=47, pos="RB", matchup="AvB"),
Player(
name="J",
cost=5500,
proj=480,
pos="TE",
possible_positions="TE/WR",
multi_position=True,
matchup="AvB",
),
Player(
name="J",
Expand All @@ -601,10 +606,11 @@ def test_multi_position_group_constraint():
pos="WR",
possible_positions="TE/WR",
multi_position=True,
matchup="AvB",
),
Player(name="K", cost=5500, proj=49, pos="TE"),
Player(name="L", cost=5500, proj=51, pos="DST"),
Player(name="M", cost=5500, proj=52, pos="DST"),
Player(name="K", cost=5500, proj=49, pos="TE", matchup="CvD"),
Player(name="L", cost=5500, proj=51, pos="DST", matchup="CvD"),
Player(name="M", cost=5500, proj=52, pos="DST", matchup="CvD"),
]

grouped_players = ("A", "C", "J")
Expand Down Expand Up @@ -719,12 +725,14 @@ def test_no_opposing_def_dk_nfl_mock():
]

# relax min teams for simplified player pool
rules.DK_NFL_RULE_SET.min_matchups = 1
rules.DK_NFL_RULE_SET.min_teams = 1

# mock pool is constructed such that optimal lineup has qb opposing dst
roster = run(
rule_set=rules.DK_NFL_RULE_SET, player_pool=mock_pool, verbose=True
)
rules.DK_NFL_RULE_SET.min_teams = 3
rules.DK_NFL_RULE_SET.min_matchups = 2

assertions.assertEqual(roster.projected(), 909)
qb_team = roster.sorted_players()[0].team
Expand All @@ -741,6 +749,7 @@ def test_no_opposing_def_dk_nfl_mock():

assertions.assertEqual(roster, None)

rules.DK_NFL_RULE_SET.min_matchups = 1
rules.DK_NFL_RULE_SET.min_teams = 1
roster = run(
rule_set=rules.DK_NFL_RULE_SET,
Expand All @@ -754,7 +763,7 @@ def test_no_opposing_def_dk_nfl_mock():
assertions.assertEqual(roster.projected(), 877)
assertions.assertEqual(len(set([p.team for p in roster.players])), 1)

# add a player from a third team, min 2 teams
# add a player from a second matchup, min two matchups
mock_pool.append(
Player(
name="B2", cost=5500, proj=70, pos="QB", team="Q", matchup="Q@Z"
Expand All @@ -771,7 +780,8 @@ def test_no_opposing_def_dk_nfl_mock():
if p.pos in rules.DK_NFL_RULE_SET.offensive_positions:
assertions.assertNotEqual(p.team, "X")

rules.DK_NFL_RULE_SET.min_teams = 3
rules.DK_NFL_RULE_SET.min_matchups = 2
rules.DK_NFL_RULE_SET.min_teams = 2


def test_no_opposing_def_dk_nfl():
Expand Down
Loading

0 comments on commit 23c7d31

Please sign in to comment.