fplkit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fpl_cli/__init__.py +6 -0
- fpl_cli/_version.py +24 -0
- fpl_cli/agents/__init__.py +5 -0
- fpl_cli/agents/action/__init__.py +1 -0
- fpl_cli/agents/action/waiver.py +530 -0
- fpl_cli/agents/analysis/__init__.py +1 -0
- fpl_cli/agents/analysis/bench_order.py +275 -0
- fpl_cli/agents/analysis/captain.py +243 -0
- fpl_cli/agents/analysis/squad_analyzer.py +370 -0
- fpl_cli/agents/analysis/starting_xi.py +191 -0
- fpl_cli/agents/analysis/stats.py +752 -0
- fpl_cli/agents/analysis/transfer_eval.py +281 -0
- fpl_cli/agents/base.py +164 -0
- fpl_cli/agents/common.py +224 -0
- fpl_cli/agents/data/__init__.py +1 -0
- fpl_cli/agents/data/fixture.py +517 -0
- fpl_cli/agents/data/price.py +216 -0
- fpl_cli/agents/data/scout.py +220 -0
- fpl_cli/agents/orchestration/__init__.py +1 -0
- fpl_cli/agents/orchestration/report.py +595 -0
- fpl_cli/api/__init__.py +1 -0
- fpl_cli/api/football_data.py +140 -0
- fpl_cli/api/fpl.py +306 -0
- fpl_cli/api/fpl_draft.py +392 -0
- fpl_cli/api/providers/__init__.py +152 -0
- fpl_cli/api/providers/_models.py +31 -0
- fpl_cli/api/providers/anthropic.py +136 -0
- fpl_cli/api/providers/openai_compat.py +149 -0
- fpl_cli/api/providers/perplexity.py +91 -0
- fpl_cli/api/understat.py +386 -0
- fpl_cli/api/vaastav.py +395 -0
- fpl_cli/cli/__init__.py +84 -0
- fpl_cli/cli/_banner.py +27 -0
- fpl_cli/cli/_context.py +170 -0
- fpl_cli/cli/_fines.py +221 -0
- fpl_cli/cli/_fines_config.py +76 -0
- fpl_cli/cli/_helpers.py +235 -0
- fpl_cli/cli/_json.py +92 -0
- fpl_cli/cli/_league_recap_data.py +751 -0
- fpl_cli/cli/_league_recap_types.py +111 -0
- fpl_cli/cli/_plan_grid.py +222 -0
- fpl_cli/cli/_review_analysis.py +341 -0
- fpl_cli/cli/_review_classic.py +494 -0
- fpl_cli/cli/_review_draft.py +430 -0
- fpl_cli/cli/_review_summarisation.py +666 -0
- fpl_cli/cli/allocate.py +304 -0
- fpl_cli/cli/captain.py +119 -0
- fpl_cli/cli/chips.py +487 -0
- fpl_cli/cli/credentials.py +46 -0
- fpl_cli/cli/differentials.py +164 -0
- fpl_cli/cli/fdr.py +497 -0
- fpl_cli/cli/fixtures.py +127 -0
- fpl_cli/cli/history.py +81 -0
- fpl_cli/cli/init.py +575 -0
- fpl_cli/cli/league.py +271 -0
- fpl_cli/cli/league_recap.py +292 -0
- fpl_cli/cli/player.py +863 -0
- fpl_cli/cli/preview.py +641 -0
- fpl_cli/cli/price_changes.py +107 -0
- fpl_cli/cli/price_history.py +226 -0
- fpl_cli/cli/ratings.py +229 -0
- fpl_cli/cli/review.py +371 -0
- fpl_cli/cli/sell_prices.py +206 -0
- fpl_cli/cli/squad.py +182 -0
- fpl_cli/cli/stats.py +356 -0
- fpl_cli/cli/status.py +689 -0
- fpl_cli/cli/targets.py +134 -0
- fpl_cli/cli/transfer_eval.py +287 -0
- fpl_cli/cli/waivers.py +154 -0
- fpl_cli/cli/xg.py +135 -0
- fpl_cli/config/defaults.yaml +51 -0
- fpl_cli/constants.py +3 -0
- fpl_cli/models/__init__.py +17 -0
- fpl_cli/models/chip_plan.py +106 -0
- fpl_cli/models/fixture.py +142 -0
- fpl_cli/models/player.py +216 -0
- fpl_cli/models/team.py +49 -0
- fpl_cli/models/types.py +139 -0
- fpl_cli/parsers/__init__.py +0 -0
- fpl_cli/parsers/recommendations.py +145 -0
- fpl_cli/paths.py +104 -0
- fpl_cli/prompts/__init__.py +1 -0
- fpl_cli/prompts/league_recap.py +134 -0
- fpl_cli/prompts/review.py +595 -0
- fpl_cli/prompts/scout.py +130 -0
- fpl_cli/scraper/__init__.py +1 -0
- fpl_cli/scraper/fpl_prices.py +519 -0
- fpl_cli/season.py +76 -0
- fpl_cli/services/__init__.py +1 -0
- fpl_cli/services/fixture_predictions.py +351 -0
- fpl_cli/services/matchup.py +254 -0
- fpl_cli/services/player_prior.py +233 -0
- fpl_cli/services/player_scoring.py +1742 -0
- fpl_cli/services/squad_allocator.py +667 -0
- fpl_cli/services/team_form.py +108 -0
- fpl_cli/services/team_ratings.py +681 -0
- fpl_cli/services/team_ratings_prior.py +291 -0
- fpl_cli/templates/gw_league_recap.md.j2 +67 -0
- fpl_cli/templates/gw_preview.md.j2 +128 -0
- fpl_cli/templates/gw_review.md.j2 +206 -0
- fpl_cli/utils/__init__.py +0 -0
- fpl_cli/utils/text.py +10 -0
- fplkit-1.0.0.dist-info/METADATA +312 -0
- fplkit-1.0.0.dist-info/RECORD +107 -0
- fplkit-1.0.0.dist-info/WHEEL +4 -0
- fplkit-1.0.0.dist-info/entry_points.txt +2 -0
- fplkit-1.0.0.dist-info/licenses/LICENSE +21 -0
fpl_cli/__init__.py
ADDED
fpl_cli/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Action agents."""
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""Waiver agent for recommending and managing draft waiver claims."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from fpl_cli.agents.base import Agent, AgentResult, AgentStatus
|
|
9
|
+
from fpl_cli.agents.common import (
|
|
10
|
+
enrich_player,
|
|
11
|
+
fetch_understat_lookup,
|
|
12
|
+
)
|
|
13
|
+
from fpl_cli.api.fpl import FPLClient
|
|
14
|
+
from fpl_cli.api.fpl_draft import FPLDraftClient
|
|
15
|
+
from fpl_cli.models.types import EnrichedPlayer, WaiverTarget
|
|
16
|
+
from fpl_cli.services.player_scoring import (
|
|
17
|
+
apply_shrinkage,
|
|
18
|
+
build_player_evaluation,
|
|
19
|
+
calculate_waiver_score,
|
|
20
|
+
compute_aggregate_matchup,
|
|
21
|
+
compute_form_trajectory,
|
|
22
|
+
prepare_scoring_data,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WaiverAgent(Agent):
|
|
27
|
+
"""Agent for analyzing and recommending waiver claims.
|
|
28
|
+
|
|
29
|
+
Responsibilities:
|
|
30
|
+
- Analyze available players for waiver potential
|
|
31
|
+
- Compare against current team needs
|
|
32
|
+
- Rank waiver targets with reasoning
|
|
33
|
+
- Track waiver deadline
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name = "WaiverAgent"
|
|
37
|
+
description = "Recommends waiver claims for draft leagues"
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
40
|
+
super().__init__(config)
|
|
41
|
+
self.client = FPLDraftClient()
|
|
42
|
+
self.fpl_client = FPLClient()
|
|
43
|
+
self.league_id = config.get("draft_league_id") if config else None
|
|
44
|
+
self.entry_id = config.get("draft_entry_id") if config else None
|
|
45
|
+
|
|
46
|
+
async def close(self) -> None:
|
|
47
|
+
await self.client.close()
|
|
48
|
+
await self.fpl_client.close()
|
|
49
|
+
|
|
50
|
+
async def run(self, context: dict[str, Any] | None = None) -> AgentResult:
|
|
51
|
+
"""Analyze and recommend waiver targets.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
context: Can contain:
|
|
55
|
+
- 'league_id': Draft league ID
|
|
56
|
+
- 'entry_id': Your draft entry ID
|
|
57
|
+
- 'available_players': Pre-fetched available players
|
|
58
|
+
- 'current_team': Your current squad
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
AgentResult with waiver recommendations.
|
|
62
|
+
"""
|
|
63
|
+
league_id = (context or {}).get("league_id") or self.league_id
|
|
64
|
+
entry_id = (context or {}).get("entry_id") or self.entry_id
|
|
65
|
+
|
|
66
|
+
if not league_id:
|
|
67
|
+
return self._create_result(
|
|
68
|
+
AgentStatus.FAILED,
|
|
69
|
+
message="No draft league ID provided",
|
|
70
|
+
errors=["Set draft_league_id in config or provide in context"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self.log(f"Analyzing waiver options for league {league_id}...")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Fetch data
|
|
77
|
+
bootstrap = await self.client.get_bootstrap_static()
|
|
78
|
+
teams_data = bootstrap.get("teams", [])
|
|
79
|
+
team_map = {t["id"]: t for t in teams_data}
|
|
80
|
+
|
|
81
|
+
# Get available players
|
|
82
|
+
available = await self.client.get_available_players(league_id, bootstrap)
|
|
83
|
+
|
|
84
|
+
# Get recently released players (independent of entry_id)
|
|
85
|
+
recent_releases: list[dict[str, Any]] = []
|
|
86
|
+
league_details: dict[str, Any] = {}
|
|
87
|
+
try:
|
|
88
|
+
releases = await self.client.get_recent_releases(league_id, bootstrap)
|
|
89
|
+
# Build entry->name map to resolve who dropped each player
|
|
90
|
+
league_details = await self.client.get_league_details(league_id)
|
|
91
|
+
league_entries = league_details.get("league_entries", [])
|
|
92
|
+
entry_name_map = {
|
|
93
|
+
e.get("entry_id"): f"{e.get('player_first_name', '')} {e.get('player_last_name', '')}".strip()
|
|
94
|
+
for e in league_entries
|
|
95
|
+
}
|
|
96
|
+
recent_releases = [
|
|
97
|
+
{
|
|
98
|
+
**enrich_player(self.client.parse_player(r["player"]), team_map),
|
|
99
|
+
"dropped_by": entry_name_map.get(r["dropped_by"], str(r["dropped_by"])),
|
|
100
|
+
"gameweek": r["gameweek"],
|
|
101
|
+
}
|
|
102
|
+
for r in releases[:20]
|
|
103
|
+
]
|
|
104
|
+
except Exception as e: # noqa: BLE001 — best-effort enrichment
|
|
105
|
+
self.log_warning(f"Could not fetch recent releases: {e}")
|
|
106
|
+
|
|
107
|
+
# Parse players
|
|
108
|
+
parsed_available = [
|
|
109
|
+
enrich_player(self.client.parse_player(p), team_map, include_availability=False)
|
|
110
|
+
for p in available
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# Enrich with Understat data
|
|
114
|
+
us_lookup = await fetch_understat_lookup(
|
|
115
|
+
cast(list[dict[str, Any]], parsed_available),
|
|
116
|
+
lambda p: p.get("team_name", ""),
|
|
117
|
+
)
|
|
118
|
+
for i, us_match in us_lookup.items():
|
|
119
|
+
parsed_available[i]["npxG_per_90"] = us_match.get("npxG_per_90")
|
|
120
|
+
parsed_available[i]["xGChain_per_90"] = us_match.get("xGChain_per_90")
|
|
121
|
+
parsed_available[i]["penalty_xG_per_90"] = us_match.get("penalty_xG_per_90")
|
|
122
|
+
|
|
123
|
+
# Get current squad if entry_id provided
|
|
124
|
+
current_squad = []
|
|
125
|
+
squad_by_position = {"GK": [], "DEF": [], "MID": [], "FWD": []}
|
|
126
|
+
|
|
127
|
+
if entry_id:
|
|
128
|
+
try:
|
|
129
|
+
# Reuse league_details from recent releases fetch, or fetch fresh
|
|
130
|
+
if not league_details:
|
|
131
|
+
league_details = await self.client.get_league_details(league_id)
|
|
132
|
+
league_entries = league_details.get("league_entries", [])
|
|
133
|
+
|
|
134
|
+
# Find matching entry - entry_id could be entry_id or id
|
|
135
|
+
# The API uses entry_id for fetching picks
|
|
136
|
+
draft_entry_id = None
|
|
137
|
+
for entry in league_entries:
|
|
138
|
+
if entry.get("entry_id") == entry_id or entry.get("id") == entry_id:
|
|
139
|
+
draft_entry_id = entry.get("entry_id") # Use entry_id for API calls
|
|
140
|
+
self.log(f"Found team: {entry.get('entry_name')}")
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
if draft_entry_id:
|
|
144
|
+
# Get current gameweek
|
|
145
|
+
game_data = await self.client.get_game_state()
|
|
146
|
+
current_gw = game_data.get("current_event", 1)
|
|
147
|
+
|
|
148
|
+
# Fetch picks for current gameweek
|
|
149
|
+
picks_data = await self.client.get_entry_picks(draft_entry_id, current_gw)
|
|
150
|
+
player_ids = [p.get("element") for p in picks_data.get("picks", [])]
|
|
151
|
+
|
|
152
|
+
player_map = {p["id"]: p for p in bootstrap.get("elements", [])}
|
|
153
|
+
for pid in player_ids:
|
|
154
|
+
if pid in player_map:
|
|
155
|
+
player = enrich_player(
|
|
156
|
+
self.client.parse_player(player_map[pid]),
|
|
157
|
+
team_map,
|
|
158
|
+
include_availability=False,
|
|
159
|
+
)
|
|
160
|
+
current_squad.append(player)
|
|
161
|
+
pos = player.get("position", "???")
|
|
162
|
+
if pos in squad_by_position:
|
|
163
|
+
squad_by_position[pos].append(player)
|
|
164
|
+
else:
|
|
165
|
+
self.log_warning(f"Entry ID {entry_id} not found in league entries")
|
|
166
|
+
except Exception as e: # noqa: BLE001 — best-effort enrichment
|
|
167
|
+
self.log_warning(f"Could not fetch current squad: {e}")
|
|
168
|
+
|
|
169
|
+
# Fetch fixture data and build shared scoring context
|
|
170
|
+
data = await prepare_scoring_data(
|
|
171
|
+
self.fpl_client, include_players=True,
|
|
172
|
+
include_history=True, include_prior=True,
|
|
173
|
+
)
|
|
174
|
+
next_gw_id = data.next_gw_id
|
|
175
|
+
self._player_histories = data.player_histories or {}
|
|
176
|
+
self._player_priors = data.player_priors
|
|
177
|
+
scoring_ctx = data.scoring_ctx
|
|
178
|
+
|
|
179
|
+
# Enrich available players with matchup and FDR
|
|
180
|
+
matchup_cache: dict[tuple[int, str], float] = {}
|
|
181
|
+
for player in parsed_available:
|
|
182
|
+
tid = player.get("team_id", 0)
|
|
183
|
+
pos = player.get("position", "MID")
|
|
184
|
+
matchup_avg_3gw, positional_fdr = compute_aggregate_matchup(
|
|
185
|
+
tid, pos, scoring_ctx, matchup_cache=matchup_cache,
|
|
186
|
+
)
|
|
187
|
+
if matchup_avg_3gw is not None:
|
|
188
|
+
player["matchup_avg_3gw"] = matchup_avg_3gw
|
|
189
|
+
if positional_fdr is not None:
|
|
190
|
+
player["positional_fdr"] = positional_fdr
|
|
191
|
+
|
|
192
|
+
# Score and rank waiver targets
|
|
193
|
+
waiver_targets = self._rank_waiver_targets(
|
|
194
|
+
parsed_available,
|
|
195
|
+
current_squad,
|
|
196
|
+
squad_by_position,
|
|
197
|
+
next_gw_id=next_gw_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Get waiver priority
|
|
201
|
+
waiver_order = await self.client.get_waiver_order(league_id)
|
|
202
|
+
|
|
203
|
+
# Find our position in waiver order
|
|
204
|
+
our_waiver_position = None
|
|
205
|
+
if entry_id:
|
|
206
|
+
for i, team in enumerate(waiver_order, 1):
|
|
207
|
+
if team.get("entry_id") == entry_id:
|
|
208
|
+
our_waiver_position = i
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
self.log_success(f"Found {len(waiver_targets)} potential waiver targets")
|
|
212
|
+
|
|
213
|
+
return self._create_result(
|
|
214
|
+
AgentStatus.SUCCESS,
|
|
215
|
+
data={
|
|
216
|
+
"league_id": league_id,
|
|
217
|
+
"entry_id": entry_id,
|
|
218
|
+
"waiver_position": our_waiver_position,
|
|
219
|
+
"total_waiver_teams": len(waiver_order),
|
|
220
|
+
"top_targets": waiver_targets[:15],
|
|
221
|
+
"targets_by_position": self._group_by_position(waiver_targets[:30]),
|
|
222
|
+
"current_squad": current_squad,
|
|
223
|
+
"squad_weaknesses": self._identify_weaknesses(squad_by_position),
|
|
224
|
+
"recommendations": self._generate_recommendations(
|
|
225
|
+
waiver_targets,
|
|
226
|
+
squad_by_position,
|
|
227
|
+
),
|
|
228
|
+
"recent_releases": recent_releases,
|
|
229
|
+
},
|
|
230
|
+
message=f"Top waiver target: {waiver_targets[0]['player_name'] if waiver_targets else 'None'}",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
except Exception as e: # noqa: BLE001 — agent top-level handler
|
|
234
|
+
self.log_error(f"Failed to analyze waivers: {e}")
|
|
235
|
+
return self._create_result(
|
|
236
|
+
AgentStatus.FAILED,
|
|
237
|
+
message="Failed to analyze waiver options",
|
|
238
|
+
errors=[str(e)],
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _get_team_exposure(
|
|
242
|
+
self,
|
|
243
|
+
squad_by_position: dict[str, list],
|
|
244
|
+
) -> dict[str, int]:
|
|
245
|
+
"""Count players per team in current squad."""
|
|
246
|
+
team_counts: dict[str, int] = defaultdict(int)
|
|
247
|
+
for players in squad_by_position.values():
|
|
248
|
+
for p in players:
|
|
249
|
+
team_short = p.get("team_short", "???")
|
|
250
|
+
team_counts[team_short] += 1
|
|
251
|
+
return dict(team_counts)
|
|
252
|
+
|
|
253
|
+
def _check_team_exposure(
|
|
254
|
+
self,
|
|
255
|
+
target: WaiverTarget,
|
|
256
|
+
drop_candidate: dict[str, Any] | None,
|
|
257
|
+
team_counts: dict[str, int],
|
|
258
|
+
) -> tuple[int, str | None]:
|
|
259
|
+
"""Check resulting team exposure after a waiver.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
(new_count, warning_message or None)
|
|
263
|
+
"""
|
|
264
|
+
target_team = target.get("team_short", "???")
|
|
265
|
+
drop_team = drop_candidate.get("team_short") if drop_candidate else None
|
|
266
|
+
|
|
267
|
+
current = team_counts.get(target_team, 0)
|
|
268
|
+
|
|
269
|
+
# If dropping from same team, net change is 0
|
|
270
|
+
if drop_team == target_team:
|
|
271
|
+
return current, None
|
|
272
|
+
|
|
273
|
+
new_count = current + 1
|
|
274
|
+
|
|
275
|
+
if new_count >= 4:
|
|
276
|
+
return new_count, f"Heavy exposure: {new_count} {target_team} players"
|
|
277
|
+
elif new_count == 3:
|
|
278
|
+
return new_count, f"Triple-up: 3 {target_team} players"
|
|
279
|
+
|
|
280
|
+
return new_count, None
|
|
281
|
+
|
|
282
|
+
def _rank_waiver_targets(
|
|
283
|
+
self,
|
|
284
|
+
available: list[EnrichedPlayer],
|
|
285
|
+
current_squad: list[dict[str, Any]],
|
|
286
|
+
squad_by_position: dict[str, list],
|
|
287
|
+
next_gw_id: int = 38,
|
|
288
|
+
) -> list[WaiverTarget]:
|
|
289
|
+
"""Rank available players as waiver targets."""
|
|
290
|
+
scored_players = []
|
|
291
|
+
team_counts = self._get_team_exposure(squad_by_position)
|
|
292
|
+
|
|
293
|
+
for player in available:
|
|
294
|
+
score = self._calculate_waiver_score(
|
|
295
|
+
player, squad_by_position, team_counts, next_gw_id=next_gw_id,
|
|
296
|
+
)
|
|
297
|
+
reasons = self._generate_target_reasons(player, squad_by_position)
|
|
298
|
+
|
|
299
|
+
scored_players.append({
|
|
300
|
+
**player, # superset of WaiverTarget keys via EnrichedPlayer
|
|
301
|
+
"waiver_score": score,
|
|
302
|
+
"reasons": reasons,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
# Apply early-season shrinkage
|
|
306
|
+
apply_shrinkage(scored_players, "waiver_score", self._player_priors, next_gw_id)
|
|
307
|
+
|
|
308
|
+
# Sort by waiver score
|
|
309
|
+
scored_players.sort(key=lambda p: p["waiver_score"], reverse=True)
|
|
310
|
+
return scored_players
|
|
311
|
+
|
|
312
|
+
def _calculate_waiver_score(
|
|
313
|
+
self,
|
|
314
|
+
player: EnrichedPlayer,
|
|
315
|
+
squad_by_position: dict[str, list],
|
|
316
|
+
team_counts: dict[str, int] | None = None,
|
|
317
|
+
next_gw_id: int = 38,
|
|
318
|
+
) -> int:
|
|
319
|
+
"""Calculate a waiver priority score via the player scoring engine."""
|
|
320
|
+
enrichment: dict[str, Any] = {}
|
|
321
|
+
histories = getattr(self, "_player_histories", {})
|
|
322
|
+
history = histories.get(player.get("id", 0), [])
|
|
323
|
+
if history:
|
|
324
|
+
enrichment["form_trajectory"] = compute_form_trajectory(history, next_gw_id)
|
|
325
|
+
priors = getattr(self, "_player_priors", None)
|
|
326
|
+
if priors:
|
|
327
|
+
prior = priors.get(player.get("id", 0))
|
|
328
|
+
if prior:
|
|
329
|
+
enrichment["prior_confidence"] = prior.confidence
|
|
330
|
+
evaluation, _ = build_player_evaluation(
|
|
331
|
+
player,
|
|
332
|
+
enrichment=enrichment,
|
|
333
|
+
matchup_avg_3gw=player.get("matchup_avg_3gw"),
|
|
334
|
+
positional_fdr=player.get("positional_fdr"),
|
|
335
|
+
)
|
|
336
|
+
return calculate_waiver_score(
|
|
337
|
+
evaluation,
|
|
338
|
+
squad_by_position=squad_by_position,
|
|
339
|
+
team_counts=team_counts,
|
|
340
|
+
next_gw_id=next_gw_id,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _generate_target_reasons(
|
|
344
|
+
self,
|
|
345
|
+
player: EnrichedPlayer,
|
|
346
|
+
squad_by_position: dict[str, list],
|
|
347
|
+
) -> list[str]:
|
|
348
|
+
"""Generate reasons why a player is a good waiver target."""
|
|
349
|
+
reasons = []
|
|
350
|
+
|
|
351
|
+
form = player.get("form", 0)
|
|
352
|
+
if form >= 6:
|
|
353
|
+
reasons.append(f"Excellent form ({form})")
|
|
354
|
+
elif form >= 4:
|
|
355
|
+
reasons.append(f"Good form ({form})")
|
|
356
|
+
|
|
357
|
+
ppg = player.get("ppg", 0)
|
|
358
|
+
if ppg >= 5:
|
|
359
|
+
reasons.append(f"Strong PPG ({ppg:.1f})")
|
|
360
|
+
|
|
361
|
+
xgi = player.get("xGI_per_90", 0)
|
|
362
|
+
if xgi >= 0.4:
|
|
363
|
+
reasons.append(f"High xGI ({xgi:.2f}/90)")
|
|
364
|
+
|
|
365
|
+
minutes = player.get("minutes", 0)
|
|
366
|
+
if minutes >= 1500:
|
|
367
|
+
reasons.append("Regular starter")
|
|
368
|
+
|
|
369
|
+
# Check if fills a need
|
|
370
|
+
pos_name = player.get("position", "???")
|
|
371
|
+
if pos_name in squad_by_position:
|
|
372
|
+
position_players = squad_by_position[pos_name]
|
|
373
|
+
if position_players:
|
|
374
|
+
avg_form = sum(p.get("form", 0) for p in position_players) / len(position_players)
|
|
375
|
+
if form > avg_form + 1:
|
|
376
|
+
reasons.append(f"Better than current {pos_name} options")
|
|
377
|
+
|
|
378
|
+
if not reasons:
|
|
379
|
+
reasons.append("Depth option")
|
|
380
|
+
|
|
381
|
+
return reasons
|
|
382
|
+
|
|
383
|
+
def _group_by_position(
|
|
384
|
+
self,
|
|
385
|
+
players: list[WaiverTarget],
|
|
386
|
+
) -> dict[str, list[WaiverTarget]]:
|
|
387
|
+
"""Group players by position."""
|
|
388
|
+
by_position = {"GK": [], "DEF": [], "MID": [], "FWD": []}
|
|
389
|
+
|
|
390
|
+
for player in players:
|
|
391
|
+
pos = player.get("position", "???")
|
|
392
|
+
if pos in by_position:
|
|
393
|
+
by_position[pos].append(player)
|
|
394
|
+
|
|
395
|
+
return by_position
|
|
396
|
+
|
|
397
|
+
def _identify_weaknesses(
|
|
398
|
+
self,
|
|
399
|
+
squad_by_position: dict[str, list],
|
|
400
|
+
) -> list[dict[str, Any]]:
|
|
401
|
+
"""Identify weak positions in the squad."""
|
|
402
|
+
weaknesses = []
|
|
403
|
+
|
|
404
|
+
for pos, players in squad_by_position.items():
|
|
405
|
+
if not players:
|
|
406
|
+
weaknesses.append({
|
|
407
|
+
"position": pos,
|
|
408
|
+
"severity": "high",
|
|
409
|
+
"reason": "No players at this position",
|
|
410
|
+
})
|
|
411
|
+
else:
|
|
412
|
+
avg_form = sum(p.get("form", 0) for p in players) / len(players)
|
|
413
|
+
if avg_form < 3:
|
|
414
|
+
weaknesses.append({
|
|
415
|
+
"position": pos,
|
|
416
|
+
"severity": "medium",
|
|
417
|
+
"reason": f"Low average form ({avg_form:.1f})",
|
|
418
|
+
"current_players": [p.get("player_name") for p in players],
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
return weaknesses
|
|
422
|
+
|
|
423
|
+
def _calculate_drop_priority(self, player: dict[str, Any]) -> float:
|
|
424
|
+
"""Calculate drop priority score (higher = more droppable).
|
|
425
|
+
|
|
426
|
+
Priority order:
|
|
427
|
+
1. Suspended players (status="s") - highest priority to drop
|
|
428
|
+
2. Unavailable with 0% chance of playing
|
|
429
|
+
3. Injured with low chance (<50%)
|
|
430
|
+
4. Injured with medium chance (50-75%)
|
|
431
|
+
5. Available players sorted by form (lowest form = higher priority)
|
|
432
|
+
"""
|
|
433
|
+
status = player.get("status", "a")
|
|
434
|
+
chance = player.get("chance_of_playing_next_round")
|
|
435
|
+
form = player.get("form", 0)
|
|
436
|
+
|
|
437
|
+
# Suspended players are highest priority to drop
|
|
438
|
+
if status == "s":
|
|
439
|
+
return 1000
|
|
440
|
+
|
|
441
|
+
# Unavailable with 0% chance
|
|
442
|
+
if status != "a" and chance == 0:
|
|
443
|
+
return 500
|
|
444
|
+
|
|
445
|
+
# Injured with low chance (<50%)
|
|
446
|
+
if status != "a" and chance is not None and chance < 50:
|
|
447
|
+
return 200 + (50 - chance)
|
|
448
|
+
|
|
449
|
+
# Injured with medium chance (50-75%)
|
|
450
|
+
if status != "a" and chance is not None and chance < 75:
|
|
451
|
+
return 100 + (75 - chance)
|
|
452
|
+
|
|
453
|
+
# Available players - inverse form (lower form = higher drop score)
|
|
454
|
+
return 10 - min(form, 10)
|
|
455
|
+
|
|
456
|
+
def _generate_recommendations(
|
|
457
|
+
self,
|
|
458
|
+
waiver_targets: list[WaiverTarget],
|
|
459
|
+
squad_by_position: dict[str, list],
|
|
460
|
+
) -> list[dict[str, Any]]:
|
|
461
|
+
"""Generate specific waiver recommendations."""
|
|
462
|
+
recommendations = []
|
|
463
|
+
team_counts = self._get_team_exposure(squad_by_position)
|
|
464
|
+
|
|
465
|
+
# Find best target for each position
|
|
466
|
+
seen_positions = set()
|
|
467
|
+
|
|
468
|
+
for target in waiver_targets[:20]:
|
|
469
|
+
pos = target.get("position", "???")
|
|
470
|
+
|
|
471
|
+
if pos not in seen_positions and pos in squad_by_position:
|
|
472
|
+
squad_players = squad_by_position[pos]
|
|
473
|
+
|
|
474
|
+
# Find worst player to drop (highest drop priority)
|
|
475
|
+
drop_candidate = None
|
|
476
|
+
if squad_players:
|
|
477
|
+
drop_candidate = max(
|
|
478
|
+
squad_players, key=lambda p: self._calculate_drop_priority(p)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Determine why this player is being dropped
|
|
482
|
+
drop_reason = None
|
|
483
|
+
if drop_candidate:
|
|
484
|
+
drop_status = drop_candidate.get("status", "a")
|
|
485
|
+
drop_chance = drop_candidate.get("chance_of_playing_next_round")
|
|
486
|
+
if drop_status == "s":
|
|
487
|
+
drop_reason = "Suspended"
|
|
488
|
+
elif drop_status != "a" and drop_chance == 0:
|
|
489
|
+
drop_reason = "Unavailable (0%)"
|
|
490
|
+
elif drop_status != "a" and drop_chance is not None and drop_chance < 75:
|
|
491
|
+
drop_reason = f"Doubtful ({drop_chance}%)"
|
|
492
|
+
else:
|
|
493
|
+
drop_reason = f"Low form ({drop_candidate.get('form', 0)})"
|
|
494
|
+
|
|
495
|
+
# Check team exposure
|
|
496
|
+
new_count, exposure_warning = self._check_team_exposure(
|
|
497
|
+
target, drop_candidate, team_counts
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
rec: dict[str, Any] = {
|
|
501
|
+
"priority": len(recommendations) + 1,
|
|
502
|
+
"target": {
|
|
503
|
+
"name": target.get("player_name"),
|
|
504
|
+
"team": target.get("team_short"),
|
|
505
|
+
"position": pos,
|
|
506
|
+
"form": target.get("form"),
|
|
507
|
+
"waiver_score": target.get("waiver_score"),
|
|
508
|
+
},
|
|
509
|
+
"drop": {
|
|
510
|
+
"name": drop_candidate.get("player_name") if drop_candidate else None,
|
|
511
|
+
"form": drop_candidate.get("form") if drop_candidate else None,
|
|
512
|
+
"reason": drop_reason,
|
|
513
|
+
} if drop_candidate else None,
|
|
514
|
+
"reasons": target.get("reasons", []),
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if exposure_warning:
|
|
518
|
+
rec["exposure"] = {
|
|
519
|
+
"team": target.get("team_short"),
|
|
520
|
+
"count_after": new_count,
|
|
521
|
+
"warning": exposure_warning,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
recommendations.append(rec)
|
|
525
|
+
seen_positions.add(pos)
|
|
526
|
+
|
|
527
|
+
if len(recommendations) >= 5:
|
|
528
|
+
break
|
|
529
|
+
|
|
530
|
+
return recommendations
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Analysis agents."""
|