kronos / webui /fusion_backtest_validator.py
sgfw's picture
Upload 82 files
1d900df verified
Raw History Blame Contribute Delete
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] 模块加载成功")