import math import pydantic from creation_helpers import mround, sanitize_chance_output from typing import Literal from exceptions import logger class PitchingCardRatingsModel(pydantic.BaseModel): pitchingcard_id: int pit_hand: Literal['R', 'L'] vs_hand: Literal['R', 'L'] all_hits: float = 0.0 all_other_ob: float = 0.0 all_outs: float = 0.0 rem_singles: float = 0.0 rem_xbh: float = 0.0 rem_hr: float = 0.0 rem_doubles: float = 0.0 hard_rate: float med_rate: float soft_rate: float # pull_rate: float # center_rate: float # slap_rate: float homerun: float = 0.0 bp_homerun: float = 0.0 triple: float = 0.0 double_three: float = 0.0 double_two: float = 0.0 double_cf: float = 0.0 single_two: float = 0.0 single_one: float = 0.0 single_center: float = 0.0 bp_single: float = 0.0 hbp: float = 0.0 walk: float = 0.0 strikeout: float = 0.0 rem_flyballs: float = 0.0 flyout_lf_b: float = 0.0 flyout_cf_b: float = 0.0 flyout_rf_b: float = 0.0 rem_groundballs: float = 0.0 groundout_a: float = 0.0 groundout_b: float = 0.0 xcheck_p: float = float(1.0) xcheck_c: float = float(3.0) xcheck_1b: float = float(2.0) xcheck_2b: float = float(6.0) xcheck_3b: float = float(3.0) xcheck_ss: float = float(7.0) xcheck_lf: float = float(2.0) xcheck_cf: float = float(3.0) xcheck_rf: float = float(2.0) avg: float = 0.0 obp: float = 0.0 slg: float = 0.0 def total_chances(self): return mround(sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf, self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout, self.flyout_lf_b, self.flyout_cf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.xcheck_p, self.xcheck_c, self.xcheck_1b, self.xcheck_2b, self.xcheck_3b, self.xcheck_ss, self.xcheck_lf, self.xcheck_cf, self.xcheck_rf ])) def total_hits(self): return mround(sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf, self.single_two, self.single_one, self.single_center, self.bp_single ])) def total_ob(self): return mround(sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf, self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk ])) def total_outs(self): return mround(sum([ self.strikeout, self.flyout_lf_b, self.flyout_cf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.xcheck_p, self.xcheck_c, self.xcheck_1b, self.xcheck_2b, self.xcheck_3b, self.xcheck_ss, self.xcheck_lf, self.xcheck_cf, self.xcheck_rf ])) def calculate_rate_stats(self): self.avg = mround(self.total_hits() / 108, prec=5, base=0.00001) self.obp = mround((self.total_hits() + self.hbp + self.walk) / 108, prec=5, base=0.00001) self.slg = mround(( self.homerun * 4 + self.bp_homerun * 2 + self.triple * 3 + self.double_three * 2 + self.double_two * 2 + self.double_cf * 2 + self.single_two + self.single_one + self.single_center + self.bp_single / 2) / 108, prec=5, base=0.00001) def custom_to_dict(self): self.calculate_rate_stats() return { 'pitchingcard_id': self.pitchingcard_id, 'vs_hand': self.vs_hand, 'homerun': self.homerun, 'bp_homerun': self.bp_homerun, 'triple': self.triple, 'double_three': self.double_three, 'double_two': self.double_two, 'double_cf': self.double_cf, 'single_two': self.single_two, 'single_one': self.single_one, 'single_center': self.single_center, 'bp_single': self.bp_single, 'hbp': self.hbp, 'walk': self.walk, 'strikeout': self.strikeout, 'flyout_lf_b': self.flyout_lf_b, 'flyout_cf_b': self.flyout_cf_b, 'flyout_rf_b': self.flyout_rf_b, 'groundout_a': self.groundout_a, 'groundout_b': self.groundout_b, 'xcheck_p': self.xcheck_p, 'xcheck_c': self.xcheck_c, 'xcheck_1b': self.xcheck_1b, 'xcheck_2b': self.xcheck_2b, 'xcheck_3b': self.xcheck_3b, 'xcheck_ss': self.xcheck_ss, 'xcheck_lf': self.xcheck_lf, 'xcheck_cf': self.xcheck_cf, 'xcheck_rf': self.xcheck_rf, 'avg': self.avg, 'obp': self.obp, 'slg': self.slg } def calculate_singles(self, szn_hits, szn_singles): if szn_hits == 0: return tot = sanitize_chance_output(self.all_hits * (szn_singles / szn_hits)) logger.debug(f'total singles: {tot}') self.rem_singles = tot self.bp_single = 5.0 if self.rem_singles >= 5 else 0.0 self.rem_singles -= self.bp_single self.single_two = sanitize_chance_output(self.rem_singles / 2) if self.hard_rate >= 0.2 else 0.0 self.rem_singles -= self.single_two self.single_one = sanitize_chance_output(self.rem_singles) if self.soft_rate >= .2 else 0.0 self.rem_singles -= self.single_one self.single_center = sanitize_chance_output(self.rem_singles) self.rem_singles -= self.single_center self.rem_xbh = self.all_hits - self.single_center - self.single_one - self.single_two - self.bp_single logger.info(f'remaining singles: {self.rem_singles} / total xbh: {self.rem_xbh}') def calculate_xbh(self, szn_doubles, szn_triples, szn_homeruns, hr_per_fb_rate): szn_xbh = szn_doubles + szn_triples + szn_homeruns if szn_xbh == 0: return hr_rate = mround(szn_homeruns / szn_xbh) tr_rate = mround(szn_triples / szn_xbh) do_rate = mround(szn_doubles / szn_xbh) logger.info(f'hr%: {hr_rate:.2f} / tr%: {tr_rate:.2f} / do%: {do_rate:.2f}') raw_do_chances = sanitize_chance_output(self.rem_xbh * do_rate) logger.info(f'raw do chances: {raw_do_chances}') self.double_two = raw_do_chances if self.soft_rate > .2 else 0.0 self.double_cf = mround(raw_do_chances - self.double_two) self.rem_xbh -= mround(self.double_two + self.double_cf + self.double_three) logger.info(f'Double**: {self.double_two} / Double(cf): {self.double_cf} / rem xbh: {self.rem_xbh}') self.triple = sanitize_chance_output(self.rem_xbh * tr_rate) self.rem_xbh = mround(self.rem_xbh - self.triple) logger.info(f'Triple: {self.triple} / rem xbh: {self.rem_xbh}') raw_hr_chances = self.rem_xbh logger.info(f'raw hr chances: {raw_hr_chances}') if hr_per_fb_rate < .08: self.bp_homerun = sanitize_chance_output(raw_hr_chances, min_chances=1.0, rounding=1.0) elif hr_per_fb_rate > .28: self.homerun = raw_hr_chances elif hr_per_fb_rate > .18: self.bp_homerun = sanitize_chance_output(raw_hr_chances * 0.4, min_chances=1.0, rounding=1.0) self.homerun = self.rem_xbh - self.bp_homerun else: self.bp_homerun = sanitize_chance_output(raw_hr_chances * .75, min_chances=1.0, rounding=1.0) self.homerun = mround(self.rem_xbh - self.bp_homerun) logger.info(f'BP HR: {self.bp_homerun} / ND HR: {self.homerun}') self.rem_xbh -= (self.bp_homerun + self.homerun) logger.info(f'excess xbh: {self.rem_xbh}') if self.rem_xbh > 0: if self.triple > 1: logger.info(f'Passing {self.rem_xbh} xbh to triple') self.triple += self.rem_xbh self.rem_xbh = 0.0 elif self.double_cf > 1: logger.info(f'Passing {self.rem_xbh} xbh to double(cf)') self.double_cf += self.rem_xbh self.rem_xbh = 0.0 elif self.double_two > 1: logger.info(f'Passing {self.rem_xbh} xbh to double**') self.double_two += self.rem_xbh self.rem_xbh = 0.0 elif self.single_two > 1: logger.info(f'Passing {self.rem_xbh} xbh to single**') self.single_two += self.rem_xbh self.rem_xbh = 0.0 elif self.single_center > 1: logger.info(f'Passing {self.rem_xbh} xbh to single(cf)') self.single_center += self.rem_xbh self.rem_xbh = 0.0 elif self.single_one > 1: logger.info(f'Passing {self.rem_xbh} xbh to single*') self.single_one += self.rem_xbh self.rem_xbh = 0.0 else: logger.info(f'Passing {self.rem_xbh} xbh to other_ob') self.all_other_ob += self.rem_xbh def calculate_other_ob(self, szn_walks, szn_hbp): if szn_walks + szn_hbp == 0: return this_hbp = sanitize_chance_output(self.all_other_ob * szn_hbp / (szn_walks + szn_hbp), rounding=1.0) logger.info(f'hbp value candidate: {this_hbp} / all_other_ob: {self.all_other_ob}') self.hbp = max(min(this_hbp, self.all_other_ob), 0) self.walk = mround(self.all_other_ob - self.hbp) logger.info(f'self.hbp: {self.hbp} / self.walk: {self.walk}') def calculate_strikouts(self, szn_strikeouts, szn_ab, szn_hits): denom = max(szn_ab - szn_hits, 1) raw_so = sanitize_chance_output(self.all_outs * (szn_strikeouts * 1.2) / denom) sum_bb_so = self.walk + raw_so excess = sum_bb_so - mround(math.floor(sum_bb_so)) logger.info(f'raw_so: {raw_so} / sum_bb_so: {sum_bb_so} / excess: {excess}') self.strikeout = max(raw_so - excess - .05, 0.0) if self.strikeout < 0: logger.error(f'Strikeouts are less than zero :confusedpsyduck:') def calculate_other_outs(self, fb_pct, gb_pct, oppo_pct): rem_outs = 108 - self.total_chances() all_fo = sanitize_chance_output(rem_outs * fb_pct) if self.pit_hand == 'L': self.flyout_lf_b = sanitize_chance_output(all_fo * oppo_pct) else: self.flyout_rf_b = sanitize_chance_output(all_fo * oppo_pct) self.flyout_cf_b = all_fo - self.flyout_lf_b - self.flyout_rf_b rem_outs -= (self.flyout_lf_b + self.flyout_cf_b + self.flyout_rf_b) all_gb = rem_outs self.groundout_a = sanitize_chance_output(all_gb * self.soft_rate) self.groundout_b = sanitize_chance_output(all_gb - self.groundout_a) rem_chances = 108 - self.total_chances() logger.info(f'Remaining outs: {rem_chances}') if self.strikeout > 1: logger.info(f'Passing {rem_chances} outs to strikeouts') self.strikeout += rem_chances elif self.flyout_cf_b > 1: logger.info(f'Passing {rem_chances} outs to fly(cf)') self.flyout_cf_b += rem_chances elif self.flyout_rf_b > 1: logger.info(f'Passing {rem_chances} outs to fly(rf)') self.flyout_rf_b += rem_chances elif self.flyout_lf_b > 1: logger.info(f'Passing {rem_chances} outs to fly(lf)') self.flyout_lf_b += rem_chances elif self.groundout_a > 1: logger.info(f'Passing {rem_chances} outs to gbA') self.groundout_a += rem_chances elif self.single_one > 1: logger.info(f'Passing {rem_chances} outs to single*') self.single_one += rem_chances elif self.single_center > 1: logger.info(f'Passing {rem_chances} outs to single(cf)') self.single_center += rem_chances elif self.single_two > 1: logger.info(f'Passing {rem_chances} outs to single**') self.single_two += rem_chances elif self.double_two > 1: logger.info(f'Passing {rem_chances} outs to double**') self.double_two += rem_chances elif self.double_cf > 1: logger.info(f'Passing {rem_chances} outs to double(cf)') self.double_cf += rem_chances elif self.triple > 1: logger.info(f'Passing {rem_chances} outs to triple') self.triple += rem_chances elif self.homerun > 1: logger.info(f'Passing {rem_chances} outs to homerun') self.homerun += rem_chances else: raise ValueError(f'Could not complete card')