All pastes #3KFwz38F8y Raw Edit

root

public profile only python v1 · immutable
#3KFwz38F8y ·published 2026-08-30 14:43 UTC ·by bbin123
rendered paste body
# core.pyimport numpy as npfrom scipy.stats import poissonfrom league_modules import module_registry, MODULE_EXECUTION_ORDERfrom rule_engine import RuleEngine, Contextclass FootballPredictor:    def __init__(self, **kwargs):        self.params = kwargs        defaults = {            'home_control_rate': 0.50,            'away_press_rate': 0.25,            'home_defense_loss_rate': 0.15,            'away_counter_attacks': 2.0,            'home_stadium_condition': 'normal',            'is_old_team_return': False,            'is_top_club': False,            'var_controversy': False,            'big_ball_rate_home': 0.50,            'big_ball_rate_away': 0.50,            'home_team': 'Home',            'away_team': 'Away',            'home_shots_avg': 0,            'away_shots_avg': 0,            'home_possession': 0.50,            'away_possession': 0.50,            'odds_home_handicap': 0,            'odds_draw_handicap': 0,            'odds_away_handicap': 0,        }        for key, val in defaults.items():            if key not in self.params:                self.params[key] = val        self.league_config = kwargs.get('league_config', {})        self.modules_cfg = self.league_config.get('modules', {})        self.top_clubs = self.league_config.get('top_clubs', [])        self.odds_h = self.params['odds_home']        self.odds_d = self.params['odds_draw']        self.odds_a = self.params['odds_away']        self.round = self.params['season_round']        self.context = Context()        self._init_context_from_config()        self.result = {}    def _init_context_from_config(self):        cfg = self.league_config.get('modules', {})        self.context.set('rdi_threshold', cfg.get('rdi', {}).get('threshold', 0.30))        self.context.set('uat_threshold', cfg.get('uat', {}).get('threshold', 0.75))        self.context.set('obd_threshold', cfg.get('odds_deviation', {}).get('threshold', 0.15))        self.context.set('ds_threshold', cfg.get('dynamic_pressure', {}).get('threshold', 420))        self.context.set('acf_threshold', cfg.get('adversity', {}).get('threshold', 1.00))        self.context.set('q_attack_boost', 1.0)        self.context.set('fpa_boost', 1.0)        self.context.set('kpa_boost', 1.0)        self.context.set('tcm_boost', 1.0)        self.context.set('dta_boost', 1.0)        self.context.set('pwl_boost', 1.0)        self.context.set('splitter_boost', 1.0)        self.context.set('wfl_triggered', False)        self.context.set('orc_triggered', False)        self.context.set('btp_triggered', False)        self.context.set('hfa_triggered', False)        self.context.set('obd_triggered', False)        self.context.set('drc_triggered', False)        self.context.set('rdi_triggered', False)        self.context.set('uat_triggered', False)        self.context.set('score_corrections', {})    def _calc_match_probabilities(self, xg_h, xg_a, max_goals=6):        ph = [poisson.pmf(k, xg_h) for k in range(max_goals+1)]        pa = [poisson.pmf(k, xg_a) for k in range(max_goals+1)]        score_probs = {}        win_h = draw = win_a = 0.0        for i in range(max_goals+1):            for j in range(max_goals+1):                p = ph[i] * pa[j]                score_probs[(i,j)] = p                if i > j:                    win_h += p                elif i == j:                    draw += p                else:                    win_a += p        total = win_h + draw + win_a        if total > 0:            win_h /= total            draw /= total            win_a /= total        best_score = max(score_probs, key=score_probs.get)        best_prob = score_probs[best_score]        sorted_scores = sorted(score_probs.items(), key=lambda x: -x[1])        return {            'win_h': win_h, 'draw': draw, 'win_a': win_a,            'best_score': best_score,            'best_prob': best_prob,            'score_probs': score_probs,            'sorted_scores': sorted_scores        }    def _generate_score_matrix(self, sorted_scores):        core = sorted_scores[0][0] if sorted_scores else (0,0)        defend = []        for (i,j), p in sorted_scores:            if i+j <= 2:                defend.append((i,j))                if len(defend) >= 3:                    break        if not defend:            defend = [(0,0), (1,0), (1,1)]                open_attack_cfg = self.league_config.get('open_attack', {})        threshold = open_attack_cfg.get('threshold', 0.50)        big_ball_h = self.params.get('big_ball_rate_home', 0.50)        big_ball_a = self.params.get('big_ball_rate_away', 0.50)                open_attack = (big_ball_h > threshold and big_ball_a > threshold)                total_xg = self.result.get('xg_h_after_A', 0) + self.result.get('xg_a_after_A', 0)        if not open_attack and total_xg > 2.8:            open_attack = True            self.result['_debug']['open_attack_forced'] = True                attack = []        if open_attack:            for (i,j), p in sorted_scores:                if i+j >= 3:                    attack.append((i,j))                    if len(attack) >= 3:                        break                return {'core': core, 'defend': defend, 'attack': attack, 'open_attack': open_attack}    def _generate_strategy(self):        win_h = self.result['prob_win_h']        draw = self.result['prob_draw']        win_a = self.result['prob_win_a']        strategy = {}        if win_h > 0.5:            strategy['Safe'] = f"Home Win ({self.odds_h:.2f})"        elif draw > 0.4:            strategy['Safe'] = f"Draw ({self.odds_d:.2f})"        elif win_a > 0.5:            strategy['Safe'] = f"Away Win ({self.odds_a:.2f})"        else:            strategy['Safe'] = "Double chance: Draw/Away"        if self.odds_h < 1.30:            strategy['Balanced'] = "Asian Handicap -1 / -1.5"        elif 1.90 <= self.odds_h <= 2.05 and self.result.get('ewc_trigger', False):            strategy['Balanced'] = "Home Win + Under 2.5"        elif 2.00 <= self.odds_h <= 2.30:            strategy['Balanced'] = "Home Win or Draw"        else:            strategy['Balanced'] = "Home Win / Draw"        total_xg = self.result['xg_h_after_A'] + self.result['xg_a_after_A']        if total_xg < 2.0:            strategy['Goals'] = "1, 2"        elif total_xg < 3.0:            strategy['Goals'] = "2, 3"        else:            strategy['Goals'] = "3, 4"        half1 = self._calc_match_probabilities(self.result['xg_h_half1'], self.result['xg_a_half1'])        half2 = self._calc_match_probabilities(self.result['xg_h_half2'], self.result['xg_a_half2'])        h1_state = 'W' if half1['win_h'] > 0.4 else ('D' if half1['draw'] > 0.4 else 'L')        h2_state = 'W' if half2['win_h'] > 0.4 else ('D' if half2['draw'] > 0.4 else 'L')        strategy['HT/FT'] = f"{h1_state}/{h2_state}"        self.result['strategy'] = strategy    def _generate_tags(self):        tags = []        if self.result.get('ewc_trigger', False): tags.append('Economic Win')        if self.result.get('dsr_iron_draw', False): tags.append('Draw Alert')        if self.odds_h < 1.30: tags.append('Deep Odds')        if self.params['home_stadium_condition'] != 'normal': tags.append('Home Discount')        if self.result.get('vtd_trap', False): tags.append('Trap Warning')        if self.result.get('bpr_triggered', False): tags.append('Counter Risk')        if self.result.get('open_attack', False): tags.append('Attack Mode')        if self.context.get('hfa_triggered'): tags.append('HFA Boost')        if self.context.get('btp_triggered'): tags.append('Clutch Alert')        if self.context.get('obd_triggered'): tags.append('Odds Deviation')        if self.context.get('orc_triggered'): tags.append('ORC Alert')        if self.context.get('drc_triggered'): tags.append('DRC Alert')        if self.context.get('wfl_triggered'): tags.append('WFL Signal')        if not tags: tags.append('Regular')        self.result['tags'] = tags    def _apply_injury_adjustment(self, xg_h, xg_a):        home_injuries = self.params.get('home_injuries', 0)        away_injuries = self.params.get('away_injuries', 0)        home_key_missing = self.params.get('home_key_missing', 0)        away_key_missing = self.params.get('away_key_missing', 0)        injury_cfg = self.league_config.get('injury', {})        home_penalty = (home_injuries * injury_cfg.get('home_injury_penalty', 0.04) +                        home_key_missing * injury_cfg.get('home_key_penalty', 0.08))        away_penalty = (away_injuries * injury_cfg.get('away_injury_penalty', 0.04) +                        away_key_missing * injury_cfg.get('away_key_penalty', 0.08))        min_xg = injury_cfg.get('min_xg_after_injury', 0.25)        xg_h = max(min_xg, xg_h - home_penalty)        xg_a = max(min_xg, xg_a - away_penalty)        if home_penalty > 0 or away_penalty > 0:            self.result['_debug']['injury_penalty_h'] = round(home_penalty, 3)            self.result['_debug']['injury_penalty_a'] = round(away_penalty, 3)            self.result['_debug']['xg_h_before_injury'] = round(xg_h + home_penalty, 3)            self.result['_debug']['xg_a_before_injury'] = round(xg_a + away_penalty, 3)        return xg_h, xg_a    def _apply_handicap_correction(self, score_probs, sorted_scores):        handicap_line = self.params.get('handicap_line', 0)        odds_home_h = self.params.get('odds_home_handicap', 1.90)        odds_away_h = self.params.get('odds_away_handicap', 1.90)        if handicap_line == 0 or odds_home_h == 0 or odds_away_h == 0:            return score_probs, sorted_scores        prob_home_cover = 1 / odds_home_h if odds_home_h > 0 else 0        prob_away_cover = 1 / odds_away_h if odds_away_h > 0 else 0        total = prob_home_cover + prob_away_cover        if total > 0:            prob_home_cover /= total            prob_away_cover /= total        if handicap_line < 0:            target_goals = abs(handicap_line)            if prob_home_cover > 0.55:                for (i, j), p in sorted_scores[:15]:                    if i - j >= int(target_goals) + 1:                        score_probs[(i, j)] *= 1.15                self.result['handicap_signal'] = 'Home Cover (Big Win Expected)'            else:                for (i, j), p in sorted_scores[:15]:                    if 1 <= i - j <= 2:                        score_probs[(i, j)] *= 1.10                self.result['handicap_signal'] = 'Home Win but Lose Cover (Small Win)'        elif handicap_line > 0:            if prob_away_cover > 0.55:                for (i, j), p in sorted_scores[:15]:                    if j - i >= int(handicap_line) + 1:                        score_probs[(i, j)] *= 1.15                self.result['handicap_signal'] = 'Away Cover (Big Win Expected)'            else:                for (i, j), p in sorted_scores[:15]:                    if 1 <= j - i <= 2:                        score_probs[(i, j)] *= 1.10                self.result['handicap_signal'] = 'Away Win but Lose Cover (Small Win)'        new_sorted = sorted(score_probs.items(), key=lambda x: -x[1])        return score_probs, new_sorted    # ==================== AB 线通用评分(修复版) ====================    def _apply_ab_scoring(self, xg_h, xg_a):        """        通用 AB 线评分调度器(从配置读取权重,不写死任何联赛逻辑)        基于 A 线(基本面)和 B 线(盘口)的交叉验证修正 xG        """        ab_cfg = self.league_config.get('ab_scoring', {})        if not ab_cfg.get('active', False):            return xg_h, xg_a        # 1. 计算 A 线(基本面)评分        a_cfg = ab_cfg.get('a_line', {})        home_power = self.params.get('home_power', 50)        away_power = self.params.get('away_power', 50)                # 基础战力值归一化 (50->0.5, 100->1.0)        a_score_h = 0.5 + (home_power - 50) / 100        a_score_a = 0.5 + (away_power - 50) / 100                # 伤停修正(从配置读取惩罚系数)        injuries_penalty = a_cfg.get('injuries_penalty', 0.05)        home_inj = self.params.get('home_injuries', 0) * injuries_penalty        away_inj = self.params.get('away_injuries', 0) * injuries_penalty        a_score_h = max(0.1, a_score_h - home_inj)        a_score_a = max(0.1, a_score_a - away_inj)                # 战意修正(从配置读取)        mot_boost = a_cfg.get('motivation_boost', 1.1)        if self.params.get('motivation_home', 0.5) > 0.6:            a_score_h = a_score_h * mot_boost        if self.params.get('motivation_away', 0.5) > 0.6:            a_score_a = a_score_a * mot_boost        # 2. 计算 B 线(盘口)评分        b_cfg = ab_cfg.get('b_line', {})        # 理论胜率(基于 ELO 或 power)        elo_h = self.params.get('elo_home', 1500)        elo_a = self.params.get('elo_away', 1500)        if elo_h == 1500 and elo_a == 1500:            # 如果没有 ELO,用 power 映射(50→1300, 100→1700)            elo_h = 1300 + (home_power - 50) * 8            elo_a = 1300 + (away_power - 50) * 8        theo_prob_h = 1 / (1 + 10 ** ((elo_a - elo_h) / 400))        theo_prob_a = 1 - theo_prob_h                # 市场隐含概率(从赔率提取)        implied_h = 1 / self.odds_h if self.odds_h > 0 else 0.5        implied_a = 1 / self.odds_a if self.odds_a > 0 else 0.5                # B 线评分 = 1 - 偏差(偏差越小,评分越高)        b_score_h = 1 - min(1, abs(implied_h - theo_prob_h) * 2)        b_score_a = 1 - min(1, abs(implied_a - theo_prob_a) * 2)        # 3. 交叉验证与置信度计算        val_cfg = ab_cfg.get('validation', {})        resonance_threshold = val_cfg.get('resonance_threshold', 0.1)        divergence_threshold = val_cfg.get('divergence_threshold', 0.3)        resonance_boost = val_cfg.get('resonance_boost', 1.2)        divergence_penalty = val_cfg.get('divergence_penalty', 0.7)        # 计算 A/B 线对主胜和客胜评分的平均一致性        diff_h = abs(a_score_h - b_score_h)        diff_a = abs(a_score_a - b_score_a)        avg_diff = (diff_h + diff_a) / 2        if avg_diff <= resonance_threshold:            confidence_boost = resonance_boost            self.result['ab_validation'] = 'resonance'        elif avg_diff >= divergence_threshold:            confidence_boost = divergence_penalty            self.result['ab_validation'] = 'divergence'        else:            confidence_boost = 1.0            self.result['ab_validation'] = 'neutral'        # 4. 将修正系数应用到 xG(修复版)        if confidence_boost != 1.0:            if confidence_boost > 1.0:                # 共振:双方信心都提升                if implied_h > theo_prob_h:                    # 市场更看好主队 → 主队xG提升,客队适度提升                    xg_h = xg_h * confidence_boost                    xg_a = xg_a * (1 + (confidence_boost - 1) * 0.5)                else:                    # 市场更看好客队 → 客队xG提升,主队适度提升                    xg_a = xg_a * confidence_boost                    xg_h = xg_h * (1 + (confidence_boost - 1) * 0.5)            else:                # 背离:双方信心都降低(用同一个惩罚系数,而不是一方被放大)                # 原来的 xg / confidence_boost(即 xg / 0.7)会放大 xG,这是 bug                # 修正为双方都乘以 confidence_boost(即都降低),只是降低幅度不同                if implied_h > theo_prob_h:                    # 市场更看好主队,但模型与市场背离 → 主队xG降低,客队降得更多                    xg_h = xg_h * confidence_boost                    # 客队 × (confidence_boost ^ 1.5),比主队降得更多                    xg_a = xg_a * (confidence_boost * confidence_boost)                else:                    # 市场更看好客队,但模型与市场背离 → 客队xG降低,主队降得更多                    xg_a = xg_a * confidence_boost                    # 主队 × (confidence_boost ^ 1.5),比客队降得更多                    xg_h = xg_h * (confidence_boost * confidence_boost)            self.result['ab_confidence_boost'] = confidence_boost            self.result['ab_scoring_active'] = True        return xg_h, xg_a    # ==================== 核心 predict 方法 ====================    def predict(self):        self.result['_debug'] = {}        xg_h = self.params['base_xg_home']        xg_a = self.params['base_xg_away']        self.result['_debug']['xg_h_initial'] = round(xg_h, 3)        self.result['_debug']['xg_a_initial'] = round(xg_a, 3)        # ----- 从配置读取 xG 调整 -----        xg_adj = self.league_config.get('xg_adjustments', {})        boost = xg_adj.get('default_boost', 1.10)        boost_cfg = xg_adj.get('boost_conditions', {})        odds_h = self.odds_h        odds_a = self.odds_a        league = self.params.get('league', '')        if odds_h < 1.40:            boost += boost_cfg.get('deep_odds', 0.12)        if odds_a > 4.00:            boost += boost_cfg.get('big_away_odds', 0.10)        if self.params.get('big_ball_rate_home', 0.5) > 0.55 and self.params.get('big_ball_rate_away', 0.5) > 0.55:            boost += boost_cfg.get('both_attacking', 0.12)        if self.params.get('relegation_zone', False):            boost += boost_cfg.get('relegation_zone', 0.10)        if self.params.get('first_leg_lead', 0) >= 2:            boost += boost_cfg.get('second_leg_conservative', -0.20)        if self.params.get('forward_missing', 0) >= 2:            boost += boost_cfg.get('forward_missing_penalty', -0.08)        xg_h = xg_h * xg_adj.get('home_boost_factor', 1.0)        xg_a = xg_a * xg_adj.get('away_penalty_factor', 1.0)        if self.params.get('relegation_zone', False) and xg_adj.get('relegation_home_boost', 0):            xg_h = xg_h + xg_adj.get('relegation_home_boost', 0)            xg_h = max(xg_h, xg_adj.get('relegation_min_xg', 1.0))        boost = max(xg_adj.get('boost_min', 0.85), min(xg_adj.get('boost_max', 1.60), boost))        xg_h = xg_h * boost        xg_a = xg_a * boost        self.result['_debug']['offensive_boost'] = round(boost, 3)        self.result['_debug']['boost_reasons'] = []        if odds_h < 1.40: self.result['_debug']['boost_reasons'].append('deep_odds')        if odds_a > 4.00: self.result['_debug']['boost_reasons'].append('big_away_odds')        if self.params.get('big_ball_rate_home', 0.5) > 0.55 and self.params.get('big_ball_rate_away', 0.5) > 0.55:            self.result['_debug']['boost_reasons'].append('both_attacking')        if self.params.get('first_leg_lead', 0) >= 2:            self.result['_debug']['boost_reasons'].append('second_leg_conservative')        # 西甲深盘额外加成        if league == 'LaLiga' and self.params.get('is_top_club', False) and odds_h < 1.40:            la_liga_extra = xg_adj.get('la_liga_deep_odds_extra', 0.08)            boost += la_liga_extra            self.result['_debug']['la_liga_extra_boost'] = la_liga_extra        xg_max = xg_adj.get('xg_max', 5.0)        xg_h = min(xg_h, xg_max)        xg_a = min(xg_a, xg_max)        # 西甲豪门主场保底        la_liga_floor = xg_adj.get('la_liga_home_floor', 2.2)        if league == 'LaLiga' and self.params.get('is_top_club', False) and odds_h < 1.40:            xg_h = max(xg_h, la_liga_floor)            self.result['_debug']['la_liga_home_floor_applied'] = True            self.result['_debug']['la_liga_home_floor_value'] = la_liga_floor        # 伤病调整        xg_h, xg_a = self._apply_injury_adjustment(xg_h, xg_a)                # ===== 🆕 AB 线评分(通用交叉验证) =====        xg_h, xg_a = self._apply_ab_scoring(xg_h, xg_a)                self.result['_debug']['xg_h_after_injury'] = round(xg_h, 3)        self.result['_debug']['xg_a_after_injury'] = round(xg_a, 3)        # 通用主场保底(巴西杯等)        if self.league_config.get('home_floor', {}).get('active', False):            floor_cfg = self.league_config.get('home_floor', {})            if self.params.get('is_top_club', False) and self.params.get('home_win_rate', 0) > floor_cfg.get('win_rate_threshold', 0.70):                xg_h = max(xg_h, floor_cfg.get('min_xg', 1.5))                self.result['_debug']['home_floor_applied'] = True        # ===== 模块执行(含沙特新模块) =====        pre_modules = ['wfl', 'rdi', 'uat']        for module_name in pre_modules:            if module_name in module_registry:                module_func = module_registry[module_name]                xg_h, xg_a = module_func(self, xg_h, xg_a)                self.context.set('current_xg_h', xg_h)                self.context.set('current_xg_a', xg_a)                RuleEngine.apply_rules(self.context)        for module_name in MODULE_EXECUTION_ORDER:            if module_name in module_registry and module_name not in pre_modules:                module_cfg = self.modules_cfg.get(module_name, {})                if module_cfg.get('active', True):                    module_func = module_registry[module_name]                    #print(f"▶️ 执行模块: {module_name}")                    xg_h, xg_a = module_func(self, xg_h, xg_a)                    self.context.set('current_xg_h', xg_h)                    self.context.set('current_xg_a', xg_a)                    RuleEngine.apply_rules(self.context)        # ===== 🆕 全局 xG 上限保护(防止极端强弱对话爆炸) =====        xg_global_cap = xg_adj.get('xg_global_cap', 5.0)        if xg_h > xg_global_cap:            self.result['_debug']['xg_h_capped'] = round(xg_h, 3)            xg_h = xg_global_cap        if xg_a > xg_global_cap:            self.result['_debug']['xg_a_capped'] = round(xg_a, 3)            xg_a = xg_global_cap        self.result['xg_h_after_A'] = xg_h        self.result['xg_a_after_A'] = xg_a        self.result['_debug']['xg_h_final'] = round(xg_h, 3)        self.result['_debug']['xg_a_final'] = round(xg_a, 3)        # 半全场划分        tds_config = self.modules_cfg.get('tds', {})        if tds_config.get('active', False):            early = tds_config.get('early', [0.40, 0.60])            mid = tds_config.get('mid', [0.45, 0.55])            deep = tds_config.get('deep', [0.35, 0.65])            if self.round <= 3:                half_h, half_a = early[0], early[1]            elif self.odds_h < 1.30:                half_h, half_a = deep[0], deep[1]            else:                half_h, half_a = mid[0], mid[1]        else:            half_h, half_a = 0.45, 0.55        h_h1, h_h2 = xg_h * half_h, xg_h * half_a        a_h1, a_h2 = xg_a * half_h, xg_a * half_a        if self.params.get('var_controversy', False):            a_h2 *= 1.05        self.result['xg_h_half1'] = h_h1        self.result['xg_h_half2'] = h_h2        self.result['xg_a_half1'] = a_h1        self.result['xg_a_half2'] = a_h2        # 概率计算        probs = self._calc_match_probabilities(xg_h, xg_a)        self.result['core_prob'] = probs['best_prob']        score_corrections = self.context.get('score_corrections', {})        for score_key, weight in score_corrections.items():            if score_key in probs['score_probs']:                probs['score_probs'][score_key] *= weight        probs['sorted_scores'] = sorted(probs['score_probs'].items(), key=lambda x: -x[1])        win_h, draw, win_a = probs['win_h'], probs['draw'], probs['win_a']        if self.result.get('dsr_iron_draw', False):            draw *= 1.20        if self.context.get('q_attack_boost', 1.0) != 1.0:            win_h *= self.context.get('q_attack_boost', 1.0)        if self.context.get('fpa_boost', 1.0) != 1.0:            win_h *= self.context.get('fpa_boost', 1.0)        total = win_h + draw + win_a        if total > 0:            win_h /= total            draw /= total            win_a /= total        draw_cap = self.modules_cfg.get('dsr', {}).get('draw_cap', 0.35)        if draw > draw_cap:            draw = draw_cap            total = win_h + win_a            if total > 0:                win_h = win_h / total * (1 - draw_cap)                win_a = win_a / total * (1 - draw_cap)        self.result['prob_win_h'] = win_h        self.result['prob_draw'] = draw        self.result['prob_win_a'] = win_a        if self.params.get('handicap_line', 0) != 0:            probs['score_probs'], probs['sorted_scores'] = self._apply_handicap_correction(                probs['score_probs'], probs['sorted_scores']            )        matrix = self._generate_score_matrix(probs['sorted_scores'])        self.result['score_core'] = matrix['core']        self.result['score_defend'] = matrix['defend']        self.result['score_attack'] = matrix['attack']        self.result['open_attack'] = matrix['open_attack']        self._generate_strategy()        self._generate_tags()        return self.result