Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions TechDocs/extension_value_and_tag_calculations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
Calculating Extension Values
Note: when referring to current Values, reference the Value of the entire contract, not just the remaining years (should now be saved as a separate variable with the player)

1. Assign all players a position based on where they played the most snaps (above a certain floor), with primary position as the default
a. Speed Rusher DEs and Pass Rush OLBs are grouped together in an “EDGE” category
b. DTs and all other DEs are grouped together
c. ILBs and all other OLBs are grouped together
2. For all players age 25 and younger, treat their Overall as being 4 points higher than it currently is
The following steps should be applied to each position group separately
3. Within each position group, rank players from highest Overall to lowest, using Age as a secondary metric (ranked lowest to highest)
4. For players ranked at the top of their list, set their Value expectation equal to the Max Value of any current contract from that position group, PLUS 10%. If Overall is tied for the 5th place player, include all players with the same Overall in this category
a. Exception: if the highest-Value contract is at least 150% of second-highest, exclude it from the list of current contracts to compare against (Caruso-Bordewyk Rule)
b. The top tier for each position is the following
i. 5: QB/RB/FB/TE/C/FS/SS/K/P
ii. 10: OT/OG/DT/DE/OLB/ILB

5. For players not included in #4, create a custom set of current values to compare against. Use all players at the current position except:
a. Do not consider players on rookie contracts
b. Do not consider players on UDFA contracts (soon to be irrelevant)
c. Do not consider contracts of players above a certain age
i. QB/K: 34 and older
ii. RB/FB: 27 and older
iii. All other: 29 and older

The idea here is to prevent declining veterans at the end of long deals from inflating the expectations of younger mid-tier players

6. For all players in step #5, set their Value expectation equal to the highest current Value among players of equal or lower Overall in the custom set
7. Smoothing: create a second expectation Value for each player using the outputs from step 4-6. This second number is calculated as the average of the Values from the players who are [Overall +1] and [Overall -1] compared to the current player.

This took some manual fixing from me since there could be numbers missing, especially at the high end of each group

8. Set a player’s Value expectation as the higher number between steps 4-7
9. Adjust the Value based on the Age factor, using the table appropriate for that position (see appendix). This adjusted number is the final Value expectation for that player
10. Set AAV expectation as 40% of the Value expectation

Calculating Tag Values

1. Using the same position groups from step 1 above, rank all players within each group based on current year pay (NOT contract Value), calculated as current Bonus + current Salary
a. If a player was traded, treat their Bonus as being what it was before they were traded (this also took a lot of manual work to restore Bonus amounts)
2. Calculate tag amounts based on the following averages of current year pay:
a. Franchise: average of top 5
b. Transition: average of top 10
c. Playtime: average of 3rd – 20th
d. Basic: average of 3rd – 25th

Note: the manual tags that teams can apply to any player during extension season (to automatically add one year to their current contract) always use the Franchise amount. The other tag amounts are only used to calculate 5th year option costs for 1st round draft picks. The appropriate tag is determined automatically using the following criteria:

1. Franchise: multiple Pro Bowl selections
2. Transition: one Pro Bowl selection
3. Playtime: starter-level snaps (765) in at least 2 of his first 3 seasons
4. Basic: none of the above


Appendix: Value Reduction by Age (and Position)

QB/K
Age Factor
23 15%
24 10%
25 5%
26 0%
27 -5%
28 -10%
29 -15%
30 -20%
31 -25%
32 -30%
33 -45%
34 -60%
35 -75%
36+ -90%

RB/FB
Age Factor
23 10%
24 5%
25 0%
26 -10%
27 -20%
28 -30%
29 -45%
30 -60%
31 -75%
32+ -90%

All Other
Age Factor
23 15%
24 10%
25 5%
26 0%
27 -10%
28 -20%
29 -30%
30 -40%
31 -50%
32 -60%
33 -75%
34+ -90%
20 changes: 20 additions & 0 deletions controller/OffseasonController.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,23 @@ func UpdateTeamProfileAffinities(w http.ResponseWriter, r *http.Request) {
managers.UpdateTeamProfileAffinities()
json.NewEncoder(w).Encode("Done!")
}

func CalculateExtensionValues(w http.ResponseWriter, r *http.Request) {
managers.CalculatePlayerMinimumAndAAVValues()
json.NewEncoder(w).Encode("Done!")
}

func CalculateTagValues(w http.ResponseWriter, r *http.Request) {
managers.CalculateTagValues()
json.NewEncoder(w).Encode("Done!")
}

func ResetNFLMinimumValues(w http.ResponseWriter, r *http.Request) {
managers.ResetNFLPlayerMinimumValues()
json.NewEncoder(w).Encode("Done!")
}

func SeedTagDataFromJSON(w http.ResponseWriter, r *http.Request) {
managers.SeedNFLTagDataFromJSON()
json.NewEncoder(w).Encode("Done!")
}
2 changes: 2 additions & 0 deletions dbprovider/dbprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

config "github.com/CalebRose/SimFBA/secrets"
"github.com/CalebRose/SimFBA/structs"
_ "github.com/jinzhu/gorm/dialects/mysql"
"golang.org/x/crypto/ssh"
"gorm.io/driver/mysql"
Expand Down Expand Up @@ -150,6 +151,7 @@ func (p *Provider) InitDatabase() bool {
// db.AutoMigrate(&structs.NFLWaiverOffer{})
// db.AutoMigrate(&structs.NFLUDFABoard{})
// db.AutoMigrate(&structs.NFLUDFAProfile{})
db.AutoMigrate(&structs.NFLTagData{})

// All
// db.AutoMigrate(&structs.AdminRecruitModifier{})
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ func handleRequests() http.Handler {
// Offseason
apiRouter.HandleFunc("/offseason/fix/postseason/status", controller.FixPostseasonStatus).Methods("GET")
apiRouter.HandleFunc("/offseason/update/team/profile/affinities", controller.UpdateTeamProfileAffinities).Methods("GET")
apiRouter.HandleFunc("/offseason/nfl/calculate/extension/values", controller.CalculateExtensionValues).Methods("GET")
apiRouter.HandleFunc("/offseason/nfl/calculate/tag/values", controller.CalculateTagValues).Methods("GET")
apiRouter.HandleFunc("/offseason/nfl/reset/minimum/values", controller.ResetNFLMinimumValues).Methods("GET")
apiRouter.HandleFunc("/offseason/nfl/seed/tag/data", controller.SeedTagDataFromJSON).Methods("GET")

// Player Controls
apiRouter.HandleFunc("/players/all/", controller.AllPlayers).Methods("GET")
Expand Down
34 changes: 27 additions & 7 deletions managers/FreeAgencyManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,10 @@ func SignFreeAgent(offer structs.FreeAgencyOffer, FreeAgent structs.NFLPlayer, t
db.Save(&FreeAgent)

// News Log
message := messageStart + FreeAgent.Position + " " + FreeAgent.FirstName + " " + FreeAgent.LastName + " has signed with the " + NFLTeam.TeamName + " with a contract worth approximately $" + strconv.Itoa(int(Contract.ContractValue)) + " Million Dollars."
totalValue := offer.TotalSalary + offer.TotalBonus
contractValue := offer.ContractValue
years := offer.ContractLength
message := messageStart + FreeAgent.Position + " " + FreeAgent.FirstName + " " + FreeAgent.LastName + " has signed with the " + NFLTeam.TeamName + ". Total: " + util.FormatDollarAmount(totalValue) + " CV: " + util.FormatDollarAmount(contractValue) + " Length: " + strconv.Itoa(years) + " years."
CreateNewsLog("NFL", message, "Free Agency", int(offer.TeamID), ts)
}

Expand Down Expand Up @@ -819,16 +822,22 @@ func SyncExtensionOffers() {
// Rejects offer
e.DeclineOffer(ts.NFLWeek)
player.DeclineOffer(ts.NFLWeek)
totalValue := e.TotalSalary + e.TotalBonus
contractValue := e.ContractValue
years := e.ContractLength
if e.IsRejected || player.Rejections > 2 {
message = player.Position + " " + player.FirstName + " " + player.LastName + " has rejected an extension offer from " + e.Team + " worth approximately $" + strconv.Itoa(int(e.ContractValue)) + " Million Dollars and will enter Free Agency."
message = player.Position + " " + player.FirstName + " " + player.LastName + " has rejected an extension offer from " + e.Team + ". Total: " + util.FormatDollarAmount(totalValue) + " CV: " + util.FormatDollarAmount(contractValue) + " Length: " + strconv.Itoa(years) + " years, and is no longer negotiating."
} else {
message = player.Position + " " + player.FirstName + " " + player.LastName + " has declined an extension offer from " + e.Team + " with an extension worth approximately $" + strconv.Itoa(int(e.ContractValue)) + " Million Dollars, and is still negotiating."
message = player.Position + " " + player.FirstName + " " + player.LastName + " has declined an extension offer from " + e.Team + ". Total: " + util.FormatDollarAmount(totalValue) + " CV: " + util.FormatDollarAmount(contractValue) + " Length: " + strconv.Itoa(years) + " years, and is still negotiating."
}
CreateNewsLog("NFL", message, "Free Agency", int(e.TeamID), ts)
db.Save(&player)
} else {
e.AcceptOffer()
message = player.Position + " " + player.FirstName + " " + player.LastName + " has accepted an extension offer from " + e.Team + " worth approximately $" + strconv.Itoa(int(e.ContractValue)) + " Million Dollars."
totalValue := e.TotalSalary + e.TotalBonus
contractValue := e.ContractValue
years := e.ContractLength
message = player.Position + " " + player.FirstName + " " + player.LastName + " has accepted an extension offer from " + e.Team + ". Total: " + util.FormatDollarAmount(totalValue) + " CV: " + util.FormatDollarAmount(contractValue) + " Length: " + strconv.Itoa(years) + " years."
CreateNewsLog("NFL", message, "Free Agency", int(e.TeamID), ts)
db.Save(&team)
}
Expand Down Expand Up @@ -1014,10 +1023,21 @@ func TagPlayer(tagDTO structs.NFLTagDTO) {
repository.SaveNFLTeam(nflTeam, db)
}

// Get the json file containing all tag data by position and tag type
tagDataBlob := util.GetTagObject()
// Resolve player's position group and look up tag amounts from DB (falls back to JSON)
playerGroup := getPositionGroup(nflPlayerRecord.Position, nflPlayerRecord.Archetype, nflPlayerRecord.Position)
tagAmounts := GetTagAmountsForGroup(playerGroup)
fifthYearSalary := 0.5
fifthYearBonus := tagDataBlob[tagDTO.Position][tagTypeStr]
var fifthYearBonus float64
switch tagTypeStr {
case "Franchise":
fifthYearBonus = tagAmounts.Franchise
case "Transition":
fifthYearBonus = tagAmounts.Transition
case "Playtime":
fifthYearBonus = tagAmounts.Playtime
default:
fifthYearBonus = tagAmounts.Basic
}

extensionOffer := structs.NFLExtensionOffer{
NFLPlayerID: nflPlayerRecord.ID,
Expand Down
25 changes: 18 additions & 7 deletions managers/ProgressionManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ func ProgressNFLPlayer(np structs.NFLPlayer, SeasonID string, totalSnaps, SnapsP
totalSnaps -= int(snaps.STSnaps)
posThreshold := float64(totalSnaps) * 0.8

if mostPlayedPosition != np.Position && float64(mostPlayedSnaps) > posThreshold {
if mostPlayedPosition != np.Position && float64(mostPlayedSnaps) > posThreshold && np.PositionTwo == "" {
// Designate New Position
newArchetype, archCheck := getNewArchetype(np.Position, np.Archetype, mostPlayedPosition)
// If Archhetype exists, assign new position. Otherwise, progress by old position
Expand All @@ -476,6 +476,11 @@ func ProgressNFLPlayer(np structs.NFLPlayer, SeasonID string, totalSnaps, SnapsP
} else {
mostPlayedPosition = np.Position
}
if mostPlayedPosition == "RB" && np.Position == "QB" {
// Lower the prime age of the player to that of a RB.
newPrimeAge := util.GetPrimeAge(mostPlayedPosition, newArchetype)
np.AssignPrimeAge(uint(newPrimeAge))
}
} else {
mostPlayedPosition = np.Position
}
Expand Down Expand Up @@ -1109,7 +1114,7 @@ func ProgressCollegePlayer(cp structs.CollegePlayer, SeasonID string, stats []st
totalSnaps -= int(snaps.STSnaps)
posThreshold := float64(totalSnaps) * 0.8

if mostPlayedPosition != cp.Position && float64(mostPlayedSnaps) > posThreshold {
if mostPlayedPosition != cp.Position && float64(mostPlayedSnaps) > posThreshold && cp.PositionTwo == "" {
// Designate New Position
newArchetype, archCheck := getNewArchetype(cp.Position, cp.Archetype, mostPlayedPosition)
// If Archhetype exists, assign new position. Otherwise, progress by old position
Expand All @@ -1118,6 +1123,11 @@ func ProgressCollegePlayer(cp structs.CollegePlayer, SeasonID string, stats []st
} else {
mostPlayedPosition = cp.Position
}
if mostPlayedPosition == "RB" && cp.Position == "QB" {
// Lower the prime age of the player to that of a RB.
newPrimeAge := util.GetPrimeAge(mostPlayedPosition, newArchetype)
cp.AssignPrimeAge(uint(newPrimeAge))
}
} else {
mostPlayedPosition = cp.Position
}
Expand Down Expand Up @@ -2818,8 +2828,9 @@ func DetermineIfDeclaring(player structs.CollegePlayer, avgSnaps int) bool {
// Year 3 AND redshirt == Redshirt Sophomore
// Year 2 AND redshirt == Redshirt Freshmen
// All players that are freshmen, redshirt freshmen, sophomore, redshirt sophomores, and non-redshirt juniors
isRedshirtJunior := player.Year == 4 && player.IsRedshirt
if !isRedshirtJunior {
isRedshirtSophomore := player.Year == 3 && player.IsRedshirt
isJunior := player.Year == 3 && !player.IsRedshirt
if !isJunior && !isRedshirtSophomore {
return false
}

Expand All @@ -2842,11 +2853,11 @@ func DetermineIfDeclaring(player structs.CollegePlayer, avgSnaps int) bool {
odds := util.GenerateIntFromRange(1, 100) - snapMod
if ovr > 54 && odds <= 25 {
return true
} else if ovr > 56 && odds <= 30 {
} else if ovr > 56 && odds <= 35 {
return true
} else if ovr > 57 && odds <= 35 {
} else if ovr > 57 && odds <= 45 {
return true
} else if ovr > 58 && odds <= 45 {
} else if ovr > 58 && odds <= 55 {
return true
} else if ovr > 59 && odds <= 70 {
return true
Expand Down
36 changes: 19 additions & 17 deletions managers/SchedulerManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,23 +141,25 @@ func ProcessCFBGameRequest(requestID string) {
}

game := structs.CollegeGame{
HomeTeamID: int(request.HomeTeamID),
HomeTeam: homeTeam.TeamName,
AwayTeamID: int(request.AwayTeamID),
AwayTeam: awayTeam.TeamName,
Week: int(request.Week),
WeekID: int(request.WeekID),
SeasonID: int(request.SeasonID),
StadiumID: arenaID,
Stadium: stadiumName,
City: city,
State: state,
Region: region,
TimeSlot: timeslot,
IsNeutral: request.IsNeutralSite,
IsConference: false,
IsDivisional: false,
IsSpringGame: request.IsSpringGame,
HomeTeamID: int(request.HomeTeamID),
HomeTeam: homeTeam.TeamAbbr,
HomeTeamCoach: homeTeam.Coach,
AwayTeamCoach: awayTeam.Coach,
AwayTeamID: int(request.AwayTeamID),
AwayTeam: awayTeam.TeamAbbr,
Week: int(request.Week),
WeekID: int(request.WeekID),
SeasonID: int(request.SeasonID),
StadiumID: arenaID,
Stadium: stadiumName,
City: city,
State: state,
Region: region,
TimeSlot: timeslot,
IsNeutral: request.IsNeutralSite,
IsConference: false,
IsDivisional: false,
IsSpringGame: request.IsSpringGame,
}

repository.CreateCFBGameRecordsBatch(db, []structs.CollegeGame{game}, 1)
Expand Down
Loading
Loading