-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwaiver_model.go
More file actions
304 lines (266 loc) · 7.14 KB
/
waiver_model.go
File metadata and controls
304 lines (266 loc) · 7.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// Advanced waiver wire model - scores free agents and suggests FAAB bids
// Considers tier delta, positional scarcity, and playoff schedule
package main
import (
"fmt"
"sort"
)
// Generate waiver recommendations for a league
func generateWaiverRecommendations(
league LeagueData,
freeAgentsByPos map[string][]PlayerRow,
maxRecommendations int,
isPremium bool,
) []WaiverRecommendation {
if !isPremium {
return nil // Feature gated to premium users
}
recommendations := []WaiverRecommendation{}
// Analyze each free agent
for pos, freeAgents := range freeAgentsByPos {
for _, fa := range freeAgents {
if fa.Tier == nil || fa.Tier == "" {
continue // Skip unranked players
}
// Calculate score for this free agent
rec := scoreWaiverTarget(fa, league, pos, len(freeAgents))
if rec.Score > 0 {
recommendations = append(recommendations, rec)
}
}
}
// Sort by score (descending)
sort.Slice(recommendations, func(i, j int) bool {
return recommendations[i].Score > recommendations[j].Score
})
// Limit to top N
if len(recommendations) > maxRecommendations {
recommendations = recommendations[:maxRecommendations]
}
return recommendations
}
func scoreWaiverTarget(fa PlayerRow, league LeagueData, position string, availableAtPos int) WaiverRecommendation {
score := 0
tierDelta := 0.0
impactType := "Depth Add"
rationale := ""
role := classifyWaiverRole(fa, impactType)
usageSignal := usageSignal(fa.RosterPercent)
faTier := parseTierFloat(fa.Tier)
if faTier == 0 {
return WaiverRecommendation{} // Skip unranked
}
// 1. Check if would upgrade a starter (biggest impact)
worstStarterTier := 99.0
worstStarterName := ""
for _, starter := range league.Starters {
if starter.Pos == position || (fa.IsFlex && starter.IsFlex) {
starterTier := parseTierFloat(starter.Tier)
if starterTier > worstStarterTier {
worstStarterTier = starterTier
worstStarterName = starter.Name
}
}
}
if worstStarterTier < 90 && faTier < worstStarterTier {
tierDelta = worstStarterTier - faTier
score += int(tierDelta * 30) // Major points for starter upgrades
impactType = "Starter Upgrade"
rationale = fmt.Sprintf("Would replace %s (tier %.1f → %.1f)", worstStarterName, worstStarterTier, faTier)
}
// 2. Check if would upgrade bench
if score == 0 {
worstBenchTier := 99.0
worstBenchName := ""
for _, bench := range league.Bench {
if bench.Pos == position {
benchTier := parseTierFloat(bench.Tier)
if benchTier == 0 {
benchTier = 99.0 // Unranked bench
}
if benchTier > worstBenchTier {
worstBenchTier = benchTier
worstBenchName = bench.Name
}
}
}
if worstBenchTier < 90 && faTier < worstBenchTier {
tierDelta = worstBenchTier - faTier
score += int(tierDelta * 15) // Moderate points for bench upgrades
impactType = "Depth Add"
rationale = fmt.Sprintf("Better than bench player %s", worstBenchName)
}
}
// 3. Positional scarcity bonus
scarcityScore := 0
if availableAtPos < 5 {
scarcityScore = 20 // Very scarce position
} else if availableAtPos < 10 {
scarcityScore = 10 // Somewhat scarce
} else if availableAtPos < 15 {
scarcityScore = 5 // Slight scarcity
}
score += scarcityScore
// 3b. Usage bonus from roster percentage (proxy for role/market confidence)
if fa.RosterPercent >= 70 {
score += 15
} else if fa.RosterPercent >= 40 {
score += 8
} else if fa.RosterPercent >= 20 {
score += 4
}
// 4. Dynasty value bonus (if dynasty league)
if league.IsDynasty && fa.DynastyValue > 0 {
if fa.DynastyValue > 1000 {
score += 20 // High dynasty value
} else if fa.DynastyValue > 500 {
score += 10 // Moderate dynasty value
}
}
// 5. Tier quality bonus (elite players worth more)
if faTier <= 1.5 {
score += 25 // Elite tier
} else if faTier <= 3.0 {
score += 15 // Good tier
} else if faTier <= 5.0 {
score += 5 // Decent tier
}
// 6. Check for breakout potential (young + value)
if league.IsDynasty && fa.Age > 0 && fa.Age < 25 && fa.DynastyValue > 300 {
score += 15
if impactType == "Depth Add" {
impactType = "Lottery Ticket"
rationale = fmt.Sprintf("Young breakout candidate (age %d, value %d)", fa.Age, fa.DynastyValue)
}
}
// Determine priority based on score
priority := "Low"
if score >= 70 {
priority = "High"
} else if score >= 40 {
priority = "Medium"
}
// Calculate suggested FAAB bid (percentage of budget)
suggestedBid := calculateFAABBid(score, impactType, league.HasMatchups)
// Build rationale if not set
if rationale == "" {
if faTier <= 3.0 {
rationale = fmt.Sprintf("Strong tier ranking (%.1f), %s", faTier, usageSignal)
} else {
rationale = fmt.Sprintf("Depth option at %s, %s", position, usageSignal)
}
}
if role == "" {
role = classifyWaiverRole(fa, impactType)
}
return WaiverRecommendation{
Player: fa,
Score: score,
Priority: priority,
SuggestedBid: suggestedBid,
Rationale: rationale,
ImpactType: impactType,
Role: role,
UsageSignal: usageSignal,
TierDelta: tierDelta,
PositionScarcity: availableAtPos,
}
}
func calculateFAABBid(score int, impactType string, inSeason bool) int {
// Base bid percentage
bid := 0
switch impactType {
case "Starter Upgrade":
if score >= 80 {
bid = 40 // 40% of budget for elite starter upgrade
} else if score >= 70 {
bid = 25
} else {
bid = 15
}
case "Depth Add":
if score >= 60 {
bid = 10
} else if score >= 40 {
bid = 5
} else {
bid = 2
}
case "Lottery Ticket":
bid = 8 // Young upside play
}
// Reduce bids in offseason (less urgency)
if !inSeason {
bid = bid / 2
if bid < 1 && score > 30 {
bid = 1
}
}
return bid
}
func classifyWaiverRole(fa PlayerRow, impactType string) string {
if impactType == "Starter Upgrade" {
return "Immediate Starter"
}
if impactType == "Lottery Ticket" {
return "Upside Stash"
}
if fa.RosterPercent >= 60 {
return "High-usage Depth"
}
if fa.RosterPercent >= 25 {
return "Rotational Depth"
}
return "Bench Stash"
}
func usageSignal(rosterPercent float64) string {
if rosterPercent >= 70 {
return "strong usage signal"
}
if rosterPercent >= 40 {
return "stable usage signal"
}
if rosterPercent >= 20 {
return "speculative usage signal"
}
return "low usage signal"
}
// Count starters at each position for depth analysis
func countStartersByPosition(starters []PlayerRow) map[string]int {
counts := make(map[string]int)
for _, s := range starters {
counts[s.Pos]++
}
return counts
}
// Calculate positional need score (higher = more need)
func calculatePositionalNeed(position string, league LeagueData) int {
need := 0
// Count how many starters at this position
starterCount := 0
benchCount := 0
for _, s := range league.Starters {
if s.Pos == position {
starterCount++
}
}
for _, b := range league.Bench {
if b.Pos == position {
benchCount++
}
}
totalDepth := starterCount + benchCount
// Shallow depth = higher need
if totalDepth <= 2 {
need = 30
} else if totalDepth <= 3 {
need = 20
} else if totalDepth <= 4 {
need = 10
}
// Dynasty: RB is always scarce
if league.IsDynasty && position == "RB" {
need += 15
}
return need
}