Download webui/fusion_backtest_validator.py from sgfw/kronos: direct link, hf CLI and curl.
- Browser
- Download file 20.2 kB
-
https://huggingface.co/spaces/sgfw/kronos/resolve/main/webui/fusion_backtest_validator.py
- Command line
-
hf download hf://spaces/sgfw/kronos/webui/fusion_backtest_validator.py
-
curl -L -o fusion_backtest_validator.py https://huggingface.co/spaces/sgfw/kronos/resolve/main/webui/fusion_backtest_validator.py
20.2 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| 蒸鱼融合评分回测验证引擎 v1.0 | |
| ================================ | |
| 基于FT-Transformer四维奖励机制的融合评分验证系统 | |
| 包含: Walk-Forward滚动验证 | 四维奖励评分 | 反过拟合检测 | 参数学习优化 | |
| """ | |
| import numpy as np | |
| import pandas as pd | |
| import json, os, time, math | |
| from datetime import datetime, timedelta | |
| from collections import defaultdict | |
| # ============================================================ | |
| # 一、四维奖励评分器(沿用FT奖励机制) | |
| # ============================================================ | |
| class FusionRewardScorer: | |
| """ | |
| 四维奖励评分器 - 评估融合评分对未来走势的预测质量 | |
| 维度: 趋势方向(30%) | 评分排序(25%) | 波动贴合(25%) | 信号稳定性(20%) | |
| """ | |
| def __init__(self): | |
| self.weights = { | |
| 'trend_direction': 0.30, | |
| 'score_ranking': 0.25, | |
| 'volatility_fit': 0.25, | |
| 'signal_stability': 0.20, | |
| } | |
| def score(self, fusion_scores, future_returns, future_volatilities=None): | |
| n = len(fusion_scores) | |
| if n < 5: | |
| return {'total_reward': 0, 'dimensions': {}, 'samples': n} | |
| scores = np.array(fusion_scores) | |
| returns = np.array(future_returns) | |
| vols = np.array(future_volatilities) if future_volatilities is not None else None | |
| # 1. 趋势方向得分 (30%) | |
| bullish = scores > 60 | |
| bearish = scores < 40 | |
| neutral = ~bullish & ~bearish | |
| direction_correct = np.zeros(n) | |
| direction_correct[bullish & (returns > 0)] = 1 | |
| direction_correct[bearish & (returns < 0)] = 1 | |
| direction_correct[neutral] = 0.5 | |
| trend_score = np.mean(direction_correct) * 100 | |
| confidence = np.abs(scores - 50) / 50 | |
| weighted_trend = np.sum(direction_correct * confidence) / (np.sum(confidence) + 1e-10) * 100 | |
| trend_dim = trend_score * 0.4 + weighted_trend * 0.6 | |
| # 2. 评分排序得分 (25%) | |
| if n >= 10: | |
| sorted_idx = np.argsort(scores) | |
| half = n // 2 | |
| top_returns = np.mean(returns[sorted_idx[-half:]]) | |
| bottom_returns = np.mean(returns[sorted_idx[:half]]) | |
| ranking_score = max(0, min(100, 50 + (top_returns - bottom_returns) * 500)) | |
| else: | |
| if np.std(scores) > 1 and np.std(returns) > 0.001: | |
| corr = np.corrcoef(scores, returns)[0, 1] | |
| ranking_score = max(0, min(100, 50 + corr * 50)) | |
| else: | |
| ranking_score = 50 | |
| # 3. 波动贴合得分 (25%) | |
| if vols is not None and len(vols) == n: | |
| high_conf = np.abs(scores - 50) / 50 > 0.3 | |
| if np.sum(high_conf) > 3: | |
| high_conf_vol = np.mean(vols[high_conf]) | |
| low_conf_vol = np.mean(vols[~high_conf]) if np.sum(~high_conf) > 0 else high_conf_vol | |
| vol_ratio = low_conf_vol / (high_conf_vol + 1e-10) | |
| vol_score = max(0, min(100, 50 + (vol_ratio - 1) * 30)) | |
| else: | |
| vol_score = 50 | |
| else: | |
| abs_returns = np.abs(returns) | |
| high_conf = np.abs(scores - 50) / 50 > 0.3 | |
| if np.sum(high_conf) > 3: | |
| high_conf_vol = np.mean(abs_returns[high_conf]) | |
| low_conf_vol = np.mean(abs_returns[~high_conf]) if np.sum(~high_conf) > 0 else high_conf_vol | |
| vol_ratio = low_conf_vol / (high_conf_vol + 1e-10) | |
| vol_score = max(0, min(100, 50 + (vol_ratio - 1) * 30)) | |
| else: | |
| vol_score = 50 | |
| # 4. 信号稳定性得分 (20%) | |
| if n >= 10: | |
| score_changes = np.abs(np.diff(scores)) | |
| avg_change = np.mean(score_changes) | |
| stability_score = max(0, min(100, 100 - avg_change * 4)) | |
| else: | |
| stability_score = 50 | |
| total = (trend_dim * self.weights['trend_direction'] + | |
| ranking_score * self.weights['score_ranking'] + | |
| vol_score * self.weights['volatility_fit'] + | |
| stability_score * self.weights['signal_stability']) | |
| return { | |
| 'total_reward': round(float(total), 2), | |
| 'dimensions': { | |
| 'trend_direction': round(float(trend_dim), 2), | |
| 'score_ranking': round(float(ranking_score), 2), | |
| 'volatility_fit': round(float(vol_score), 2), | |
| 'signal_stability': round(float(stability_score), 2), | |
| }, | |
| 'details': { | |
| 'trend_accuracy': round(float(trend_score), 2), | |
| 'weighted_trend_accuracy': round(float(weighted_trend), 2), | |
| 'avg_score_change': round(float(avg_change), 2) if n >= 10 else None, | |
| 'samples': n, | |
| }, | |
| 'weights': self.weights, | |
| } | |
| # ============================================================ | |
| # 二、反过拟合检测器 | |
| # ============================================================ | |
| class FusionAntiOverfit: | |
| """融合评分反过拟合检测 - 4项检验""" | |
| def __init__(self): | |
| self.results = {} | |
| def run_all(self, fusion_scores, future_returns, dates=None): | |
| scores = np.array(fusion_scores) | |
| returns = np.array(future_returns) | |
| n = len(scores) | |
| self.results = { | |
| 'ic_stability': self._test_ic_stability(scores, returns, n), | |
| 'subsample_stress': self._test_subsample_stress(scores, returns, n), | |
| 'placebo_test': self._test_placebo(scores, returns, n), | |
| 'parameter_sensitivity': self._test_parameter_sensitivity(scores, returns, n), | |
| } | |
| scores_list = [] | |
| for k, v in self.results.items(): | |
| if v.get('passed') is not None: | |
| scores_list.append(100 if v['passed'] else 0) | |
| elif v.get('score') is not None: | |
| scores_list.append(v['score']) | |
| overall = np.mean(scores_list) if scores_list else 0 | |
| self.results['overall'] = { | |
| 'score': round(float(overall), 2), | |
| 'passed': overall >= 60, | |
| 'judgment': '通过' if overall >= 60 else ('警告' if overall >= 40 else '失败'), | |
| } | |
| return self.results | |
| def _test_ic_stability(self, scores, returns, n, min_periods=3): | |
| if n < 60: | |
| return {'passed': None, 'score': 50, 'reason': '样本不足'} | |
| chunk_size = max(20, n // min_periods) | |
| periods = [] | |
| for i in range(0, n, chunk_size): | |
| end = min(i + chunk_size, n) | |
| if end - i < 10: | |
| continue | |
| s_seg = scores[i:end] | |
| r_seg = returns[i:end] | |
| if np.std(s_seg) > 0.1 and np.std(r_seg) > 0.0001: | |
| corr = np.corrcoef(s_seg, r_seg)[0, 1] | |
| else: | |
| corr = 0 | |
| periods.append(corr) | |
| if len(periods) < 2: | |
| return {'passed': None, 'score': 50, 'reason': '分段不足'} | |
| positive_rate = np.mean([1 if p > 0 else 0 for p in periods]) | |
| mean_ic = np.mean(np.abs(periods)) | |
| has_reversal = any(periods[i] * periods[i-1] < -0.1 for i in range(1, len(periods))) | |
| score = min(positive_rate / 0.6, 1) * 40 + min(mean_ic / 0.05, 1) * 40 | |
| score += 20 if not has_reversal else 0 | |
| passed = positive_rate >= 0.6 and mean_ic >= 0.03 and not has_reversal | |
| return {'passed': passed, 'score': round(float(score), 2), | |
| 'positive_rate': round(float(positive_rate), 4), | |
| 'mean_abs_ic': round(float(mean_ic), 4), | |
| 'has_reversal': has_reversal, | |
| 'period_ics': [round(float(p), 4) for p in periods], | |
| 'reason': f'正IC率={positive_rate:.0%}, 平均|IC|={mean_ic:.4f}' + (', 存在反转' if has_reversal else '')} | |
| def _test_subsample_stress(self, scores, returns, n): | |
| if n < 100: | |
| return {'passed': None, 'score': 50, 'reason': '样本不足'} | |
| abs_returns = np.abs(returns) | |
| vol_median = np.median(abs_returns) | |
| high_vol = abs_returns > vol_median | |
| low_vol = ~high_vol | |
| up_market = returns > 0 | |
| down_market = returns < 0 | |
| def _seg_ic(s, r): | |
| if len(s) < 10 or np.std(s) < 0.1 or np.std(r) < 0.0001: | |
| return 0 | |
| return np.corrcoef(s, r)[0, 1] | |
| ic_high_vol = _seg_ic(scores[high_vol], returns[high_vol]) | |
| ic_low_vol = _seg_ic(scores[low_vol], returns[low_vol]) | |
| ic_up = _seg_ic(scores[up_market], returns[up_market]) | |
| ic_down = _seg_ic(scores[down_market], returns[down_market]) | |
| ics = [ic_high_vol, ic_low_vol, ic_up, ic_down] | |
| positive_count = sum(1 for ic in ics if ic > 0) | |
| consistency = positive_count / len(ics) | |
| ic_std = np.std(ics) | |
| score = consistency * 50 + max(0, 50 - ic_std * 200) | |
| passed = consistency >= 0.75 and ic_std < 0.3 | |
| return {'passed': passed, 'score': round(float(score), 2), | |
| 'consistency': round(float(consistency), 4), | |
| 'ic_std': round(float(ic_std), 4), | |
| 'ic_high_vol': round(float(ic_high_vol), 4), | |
| 'ic_low_vol': round(float(ic_low_vol), 4), | |
| 'ic_up': round(float(ic_up), 4), 'ic_down': round(float(ic_down), 4)} | |
| def _test_placebo(self, scores, returns, n, n_shuffles=50): | |
| if n < 30: | |
| return {'passed': None, 'score': 50, 'reason': '样本不足'} | |
| real_corr = np.corrcoef(scores, returns)[0, 1] if np.std(scores) > 0.1 and np.std(returns) > 0.0001 else 0 | |
| shuffled_corrs = [] | |
| for _ in range(n_shuffles): | |
| shuffled = np.random.permutation(returns) | |
| if np.std(scores) > 0.1 and np.std(shuffled) > 0.0001: | |
| shuffled_corrs.append(np.corrcoef(scores, shuffled)[0, 1]) | |
| else: | |
| shuffled_corrs.append(0) | |
| shuffled_corrs = np.array(shuffled_corrs) | |
| rank = np.sum(shuffled_corrs >= real_corr) / len(shuffled_corrs) | |
| score = max(0, 100 - rank * 100) | |
| passed = rank < 0.05 | |
| return {'passed': passed, 'score': round(float(score), 2), | |
| 'real_corr': round(float(real_corr), 4), | |
| 'shuffle_mean': round(float(np.mean(shuffled_corrs)), 4), | |
| 'shuffle_std': round(float(np.std(shuffled_corrs)), 4), | |
| 'p_value': round(float(rank), 4), 'n_shuffles': n_shuffles} | |
| def _test_parameter_sensitivity(self, scores, returns, n): | |
| if n < 30: | |
| return {'passed': None, 'score': 50, 'reason': '样本不足'} | |
| base_signal = (scores > 60).astype(float) - (scores < 40).astype(float) | |
| perturbations = [-2, -1, 1, 2] | |
| signal_changes = [] | |
| for p in perturbations: | |
| perturbed = scores + p | |
| p_signal = (perturbed > 60).astype(float) - (perturbed < 40).astype(float) | |
| change_rate = np.mean(p_signal != base_signal) | |
| signal_changes.append(change_rate) | |
| avg_change = np.mean(signal_changes) | |
| score = max(0, 100 - avg_change * 200) | |
| passed = avg_change < 0.15 | |
| return {'passed': passed, 'score': round(float(score), 2), | |
| 'avg_signal_change': round(float(avg_change), 4), | |
| 'perturbation_results': {f'delta_{p}': round(float(c), 4) for p, c in zip(perturbations, signal_changes)}} | |
| # ============================================================ | |
| # 三、完整回测验证管道 | |
| # ============================================================ | |
| class FusionBacktestValidator: | |
| """融合评分回测验证器 - 一站式验证管道""" | |
| def __init__(self): | |
| self.reward_scorer = FusionRewardScorer() | |
| self.anti_overfit = FusionAntiOverfit() | |
| self.results = {} | |
| def validate(self, fusion_scores, future_returns, future_vols=None, dates=None): | |
| scores = np.array(fusion_scores) | |
| returns = np.array(future_returns) | |
| vols = np.array(future_vols) if future_vols is not None else None | |
| n = len(scores) | |
| if n < 10: | |
| return {'status': 'error', 'message': f'样本不足: {n}'} | |
| # 1. 四维奖励评分 | |
| reward_result = self.reward_scorer.score(scores, returns, vols) | |
| # 2. 反过拟合检测 | |
| anti_overfit_result = self.anti_overfit.run_all(scores, returns, dates) | |
| # 3. 综合评估 | |
| total_reward = reward_result['total_reward'] | |
| anti_score = anti_overfit_result['overall']['score'] | |
| composite_score = total_reward * 0.6 + anti_score * 0.4 | |
| # 4. 信号质量分析 | |
| bullish_signals = np.sum(scores > 60) | |
| bearish_signals = np.sum(scores < 40) | |
| bullish_win_rate = np.mean(returns[scores > 60] > 0) * 100 if bullish_signals > 0 else 0 | |
| bearish_win_rate = np.mean(returns[scores < 40] < 0) * 100 if bearish_signals > 0 else 0 | |
| # 评分分档收益 | |
| bins = [0, 32, 40, 46, 54, 60, 68, 100] | |
| labels = ['强烈卖出', '卖出', '偏空观望', '中性', '偏多观望', '买入', '强烈买入'] | |
| bin_returns = {} | |
| for i in range(len(bins) - 1): | |
| mask = (scores >= bins[i]) & (scores < bins[i + 1]) | |
| if np.sum(mask) > 0: | |
| bin_returns[labels[i]] = { | |
| 'count': int(np.sum(mask)), | |
| 'mean_return': round(float(np.mean(returns[mask]) * 100), 2), | |
| 'win_rate': round(float(np.mean(returns[mask] > 0) * 100), 1), | |
| } | |
| self.results = { | |
| 'status': 'success', | |
| 'samples': n, | |
| 'composite_score': round(float(composite_score), 2), | |
| 'reward': reward_result, | |
| 'anti_overfit': anti_overfit_result, | |
| 'signal_quality': { | |
| 'bullish_signals': int(bullish_signals), | |
| 'bearish_signals': int(bearish_signals), | |
| 'bullish_win_rate': round(float(bullish_win_rate), 1), | |
| 'bearish_win_rate': round(float(bearish_win_rate), 1), | |
| 'overall_win_rate': round(float(np.mean(returns > 0) * 100), 1), | |
| 'avg_return_bullish': round(float(np.mean(returns[scores > 60]) * 100), 2) if bullish_signals > 0 else 0, | |
| 'avg_return_bearish': round(float(np.mean(returns[scores < 40]) * 100), 2) if bearish_signals > 0 else 0, | |
| }, | |
| 'bin_returns': bin_returns, | |
| 'evaluation': self._generate_evaluation(composite_score, reward_result, anti_overfit_result), | |
| } | |
| return self.results | |
| def _generate_evaluation(self, composite_score, reward, anti): | |
| level = '优秀' if composite_score >= 75 else ('良好' if composite_score >= 60 else ('一般' if composite_score >= 45 else ('较差' if composite_score >= 30 else '失败'))) | |
| issues = [] | |
| if reward['total_reward'] < 50: | |
| issues.append('四维奖励得分偏低,融合评分预测能力不足') | |
| if anti['overall']['score'] < 60: | |
| issues.append('反过拟合检测未通过,存在过拟合风险') | |
| if anti.get('ic_stability', {}).get('has_reversal', False): | |
| issues.append('IC存在反转,评分稳定性不足') | |
| if anti.get('placebo_test', {}).get('passed') == False: | |
| issues.append('安慰剂检验未通过,评分可能无真实预测能力') | |
| suggestions = [] | |
| if reward['dimensions'].get('trend_direction', 0) < 50: | |
| suggestions.append('提高趋势方向权重或调整评分阈值') | |
| if reward['dimensions'].get('signal_stability', 0) < 50: | |
| suggestions.append('降低共识增强幅度,提高评分稳定性') | |
| if anti.get('parameter_sensitivity', {}).get('passed') == False: | |
| suggestions.append('降低参数敏感度,简化融合规则') | |
| return {'level': level, 'composite_score': round(float(composite_score), 2), | |
| 'issues': issues, 'suggestions': suggestions} | |
| # ============================================================ | |
| # 四、参数学习优化器 | |
| # ============================================================ | |
| class FusionParameterLearner: | |
| """融合参数学习优化器 - 基于回测奖励分数的贝叶斯风格搜索""" | |
| def __init__(self): | |
| self.best_params = {} | |
| self.search_history = [] | |
| def optimize_consensus_boost(self, fusion_func, data_loader, stock_codes, | |
| n_trials=20, verbose=True): | |
| param_grid = { | |
| 'boost_threshold': [2, 3, 4, 5], | |
| 'boost_base': [4, 6, 8, 10, 12], | |
| 'min_fusion_for_boost': [50, 55, 60, 65], | |
| 'temperature': [0.08, 0.10, 0.12, 0.15, 0.20], | |
| } | |
| best_score = -float('inf') | |
| best_params = {} | |
| validator = FusionBacktestValidator() | |
| for trial in range(n_trials): | |
| params = {k: np.random.choice(v) for k, v in param_grid.items()} | |
| try: | |
| all_scores, all_returns = [], [] | |
| for code in stock_codes[:5]: | |
| df = data_loader(code) | |
| if df is None or len(df) < 60: | |
| continue | |
| for i in range(30, len(df) - 5): | |
| slice_df = df.iloc[:i].copy() | |
| future_ret = df['close'].iloc[i+5] / df['close'].iloc[i] - 1 | |
| result = fusion_func(slice_df, params) | |
| if result and 'fusion_score' in result: | |
| all_scores.append(result['fusion_score']) | |
| all_returns.append(future_ret) | |
| if len(all_scores) < 20: | |
| continue | |
| v_result = validator.validate(all_scores, all_returns) | |
| reward = v_result['reward']['total_reward'] | |
| anti_score = v_result['anti_overfit']['overall']['score'] | |
| composite = reward * 0.5 + anti_score * 0.5 | |
| complexity_penalty = params['boost_base'] * 0.5 + (5 - params['boost_threshold']) * 1 | |
| final_score = composite - complexity_penalty * 0.5 | |
| self.search_history.append({'trial': trial + 1, 'params': params, | |
| 'reward': reward, 'anti_score': anti_score, 'composite': composite, 'final_score': final_score}) | |
| if final_score > best_score: | |
| best_score = final_score | |
| best_params = params.copy() | |
| if verbose: | |
| print(f" Trial {trial+1}/{n_trials}: reward={reward:.1f} anti={anti_score:.1f} final={final_score:.1f}") | |
| except Exception: | |
| continue | |
| self.best_params = best_params | |
| return {'best_params': best_params, 'best_score': round(float(best_score), 2), | |
| 'n_trials': len(self.search_history), | |
| 'search_history': sorted(self.search_history, key=lambda x: -x['final_score'])[:10]} | |
| # ============================================================ | |
| # 五、快速入口 | |
| # ============================================================ | |
| def run_full_validation(fusion_scores, future_returns, future_vols=None, dates=None): | |
| validator = FusionBacktestValidator() | |
| return validator.validate(fusion_scores, future_returns, future_vols, dates) | |
| if __name__ == '__main__': | |
| print("=" * 60) | |
| print(" 蒸鱼融合评分回测验证引擎 v1.0") | |
| print("=" * 60) | |
| print("\n 模块清单:") | |
| print(" FusionRewardScorer - 四维奖励评分器") | |
| print(" FusionAntiOverfit - 反过拟合检测器") | |
| print(" FusionBacktestValidator - 完整验证管道") | |
| print(" FusionParameterLearner - 参数学习优化器") | |
| print("\n [OK] 模块加载成功") | |