# -*- coding: utf-8 -*- """ 多因子机器学习股价预测引擎 - 预测引擎 ====================================== 基于FT-Transformer的多因子预测,含IC/IR验证、共线性排查 纯NumPy实现,支持CPU环境 """ import numpy as np import pandas as pd import json import os import time import sys import traceback from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from multifactor_config import ( MULTIFACTOR_MODEL_CONFIG, TRAINING_CONFIG, CHECKPOINT_DIR, REPORT_DIR ) from multifactor_feature_engineering import MultiFactorFeatureEngineer, MultiFactorDataFetcher from data_adapter import get_data_adapter # 复用FT-Transformer核心(已优化SGA梯度) from ft_transformer_model import FTTransformer, RewardMechanism, RewardWeightedLoss from training_engine import Trainer class FactorValidator: """因子有效性验证器""" def __init__(self, config=None): self.config = config or TRAINING_CONFIG['validation_constraints'] def compute_ic(self, predictions, actual_returns): """计算Information Coefficient (Spearman Rank IC)""" n = len(predictions) if n < 10: return 0.0 # 计算排名 pred_ranks = pd.Series(predictions).rank().values actual_ranks = pd.Series(actual_returns).rank().values # Pearson相关系数(对排名) pred_z = (pred_ranks - pred_ranks.mean()) / (pred_ranks.std() + 1e-10) actual_z = (actual_ranks - actual_ranks.mean()) / (actual_ranks.std() + 1e-10) ic = np.mean(pred_z * actual_z) return ic def compute_ir(self, ic_series): """计算Information Ratio (IC均值/IC标准差)""" if len(ic_series) < 3: return 0.0 return np.mean(ic_series) / (np.std(ic_series) + 1e-10) def compute_direction_accuracy(self, predictions, actual_returns): """计算方向正确率""" if len(predictions) < 5: return 0.0 pred_direction = np.sign(predictions) actual_direction = np.sign(actual_returns) correct = np.sum(pred_direction == actual_direction) return correct / len(predictions) def check_collinearity(self, feature_matrix, names=None, threshold=0.7): """检查因子共线性,返回需要剔除的因子索引""" if feature_matrix.shape[1] < 2: return [] # 标准化 X = (feature_matrix - feature_matrix.mean(axis=0)) / (feature_matrix.std(axis=0) + 1e-10) # 相关矩阵 corr = np.corrcoef(X.T) # 找高相关对 to_remove = set() n_features = corr.shape[0] for i in range(n_features): for j in range(i+1, n_features): if abs(corr[i, j]) > threshold and i not in to_remove: to_remove.add(j) removed_names = [names[i] for i in to_remove] if names else list(to_remove) return list(to_remove), removed_names def validate(self, predictions, actual_returns, feature_matrix=None, feature_names=None): """完整验证流程""" results = {} # 方向正确率 dir_acc = self.compute_direction_accuracy(predictions, actual_returns) results['direction_accuracy'] = round(dir_acc, 4) results['direction_pass'] = dir_acc >= self.config['min_direction_accuracy'] # IC ic = self.compute_ic(predictions, actual_returns) results['ic'] = round(ic, 4) results['ic_pass'] = abs(ic) >= self.config['monthly_ic_min'] # IR(模拟月度IC序列) if len(predictions) >= 20: chunk_size = len(predictions) // 5 # 分5段模拟月度 ic_series = [] for k in range(5): start = k * chunk_size end = start + chunk_size if end <= len(predictions): seg_ic = self.compute_ic(predictions[start:end], actual_returns[start:end]) ic_series.append(seg_ic) ir = self.compute_ir(np.array(ic_series)) results['ir'] = round(ir, 4) results['monthly_ic_mean'] = round(float(np.mean(ic_series)), 4) results['ir_pass'] = ir >= self.config['monthly_ir_min'] else: results['ir'] = 0.0 results['monthly_ic_mean'] = ic results['ir_pass'] = False # 共线性排查 if feature_matrix is not None and self.config['auto_prune']: remove_idx, remove_names = self.check_collinearity( feature_matrix, feature_names, self.config['max_corr_threshold'] ) results['collinear_features'] = remove_names results['collinear_count'] = len(remove_names) results['needs_pruning'] = len(remove_idx) > 0 # 总评 passed = sum([results.get('direction_pass', False), results.get('ic_pass', False), results.get('ir_pass', False)]) results['total_checks'] = 3 results['passed_checks'] = passed results['status'] = 'PASS' if passed >= 2 else 'NEEDS_IMPROVEMENT' return results class MultiFactorEngine: """多因子预测引擎""" def __init__(self): self.model = None self.feature_engineer = MultiFactorFeatureEngineer(sequence_length=20) self.validator = FactorValidator() self.feature_info = self.feature_engineer.get_feature_info() self.is_trained = False self.train_report = {} def _build_model_config(self, n_features=None): """构建模型配置""" cfg = MULTIFACTOR_MODEL_CONFIG.copy() if n_features: cfg['num_continuous_features'] = n_features cfg['num_categorical_features'] = 1 cfg['cat_cardinalities'] = [2] return cfg def train(self, df, financial_data=None, mode='quick'): """ 训练多因子模型 mode: 'quick' (模拟数据快速验证) / 'full' (真实数据完整训练) """ start_time = time.time() print("[多因子引擎] 开始训练...") if mode == 'quick': return self._quick_train() # 完整训练 features_df = self.feature_engineer.extract_all_features(df, financial_data) X_cont, X_cat, labels = self.feature_engineer.create_sequences(features_df, df) if len(X_cont) < 50: print(f"[WARN] 样本不足: {len(X_cont)}") return None n_features = X_cont.shape[2] # 数据划分 N = len(X_cont) train_size = int(N * 0.75) X_train, X_val = X_cont[:train_size], X_cont[train_size:] cat_train, cat_val = X_cat[:train_size], X_cat[train_size:] y_train, y_val = labels[:train_size], labels[train_size:] print(f" 训练集: {len(X_train)} 样本, 验证集: {len(X_val)} 样本") print(f" 特征维度: {n_features}") # 构建模型 # 快速模式使用更小的模型 cfg = self._build_model_config(n_features) cfg['d_token'] = 32 cfg['n_blocks'] = 1 cfg['n_heads'] = 2 model_config = cfg self.model = FTTransformer(model_config) # 训练 train_cfg = TRAINING_CONFIG['optimizer'].copy() if mode == 'fast': train_cfg['max_epochs'] = 10 train_cfg['early_stopping_patience'] = 3 reward = RewardMechanism({ 'trend_accuracy_weight': 0.4, 'return_mse_weight': 0.3, 'volatility_penalty_weight': 0.1, 'drawdown_penalty_weight': 0.2, }) loss_fn = RewardWeightedLoss(reward) trainer = Trainer(self.model, loss_fn, train_cfg, CHECKPOINT_DIR, REPORT_DIR) history = trainer.train(X_train, cat_train, y_train, X_val, cat_val, y_val, verbose=True) # 验证 val_preds, _ = self.model.predict(X_val, cat_val) val_results = self.validator.validate( val_preds[:, 0], y_val[:, 0], X_val[:, -1, :], # 最后时间步特征 self.feature_info['feature_names'][:n_features] if n_features <= len(self.feature_info['feature_names']) else None ) self.is_trained = True self.train_report = { 'mode': mode, 'train_samples': len(X_train), 'val_samples': len(X_val), 'n_features': n_features, 'best_epoch': trainer.best_epoch, 'best_val_loss': float(trainer.best_val_loss), 'validation': val_results, 'total_time': round(time.time() - start_time, 1), 'feature_info': { k: len(v) for k, v in self.feature_info['categories'].items() }, } print(f"\n[多因子引擎] 训练完成") print(f" 方向正确率: {val_results['direction_accuracy']:.2%}") print(f" IC: {val_results['ic']:.4f}, IR: {val_results['ir']:.4f}") print(f" 状态: {val_results['status']}") return self.model, history, val_results def _quick_train(self): """快速演示模式(模拟数据)""" start_time = time.time() print("[多因子引擎] 快速演示模式(模拟数据)") np.random.seed(42) N = 300 seq_len = 20 n_features = 46 # 总特征数 X_cont = np.random.randn(N, seq_len, n_features).astype(np.float32) X_cat = np.zeros((N, seq_len, 1), dtype=np.int32) # 模拟标签(加入信号使模型能学到东西) trend_signal = np.mean(X_cont[:, -5:, :3], axis=(1, 2)) # 用前3个特征做信号 y = np.zeros((N, 3), dtype=np.float32) y[:, 0] = np.sign(trend_signal + np.random.randn(N) * 0.5) y[:, 1] = np.tanh(trend_signal * 2) y[:, 2] = np.clip(np.abs(np.random.randn(N)) * 0.1, 0, 0.5) train_size = int(N * 0.7) # 快速模式使用更小的模型 cfg = self._build_model_config(n_features) cfg['d_token'] = 32 cfg['n_blocks'] = 1 cfg['n_heads'] = 2 model_config = cfg self.model = FTTransformer(model_config) reward = RewardMechanism({ 'weights': {'wave_structure': 0.3, 'volume_signal': 0.25, 'volume_price_fit': 0.25, 'pattern_fit': 0.2}, 'wave_structure': {'five_wave_complete_score': 100, 'three_wave_correct_score': 80, 'wave_ratio_tolerance': 0.15, 'fibonacci_alignment_bonus': 10, 'wave_count_penalty': -5}, 'volume_signal': {'double_volume_threshold': 1.8, 'volume_break_confirm_days': 3, 'shrink_volume_ratio': 0.5, 'signal_hit_score': 100, 'false_signal_penalty': -20}, 'volume_price_fit': {'price_volume_sync_score': 100, 'divergence_detect_threshold': 0.3, 'trend_confirm_bonus': 15, 'false_divergence_penalty': -15}, 'pattern_fit': {'bullish_pattern_hit': 100, 'bearish_pattern_hit': 100, 'pattern_accuracy_weight': 0.7, 'timing_accuracy_weight': 0.3}, 'fit_threshold': {'success_threshold': 0.8, 'warning_threshold': 0.6, 'failure_threshold': 0.6}, }) loss_fn = RewardWeightedLoss(reward) train_cfg = { 'batch_size': 32, 'learning_rate': 1e-2, 'max_epochs': 5, 'early_stopping_patience': 2, 'gradient_clip_norm': 1.0, 'warmup_epochs': 1, 'scheduler_type': 'cosine', 'weight_decay': 1e-5, } trainer = Trainer(self.model, loss_fn, train_cfg, CHECKPOINT_DIR, REPORT_DIR) history = trainer.train( X_cont[:train_size], X_cat[:train_size], y[:train_size], X_cont[train_size:], X_cat[train_size:], y[train_size:], verbose=True ) val_preds, _ = self.model.predict(X_cont[train_size:], X_cat[train_size:]) val_results = self.validator.validate(val_preds[:, 0], y[train_size:, 0]) self.is_trained = True self.train_report = { 'mode': 'quick', 'train_samples': train_size, 'val_samples': N - train_size, 'n_features': n_features, 'best_epoch': trainer.best_epoch, 'best_val_loss': float(trainer.best_val_loss), 'validation': val_results, 'total_time': round(time.time() - start_time, 1), 'feature_info': { k: len(v) for k, v in self.feature_info['categories'].items() }, } print(f"\n[多因子引擎] 快速演示完成: {self.train_report['total_time']}s") return self.model, history, val_results def predict(self, df, financial_data=None): """对股票做预测""" if not self.is_trained or self.model is None: return None features_df = self.feature_engineer.extract_all_features(df, financial_data) X_cont, X_cat, _ = self.feature_engineer.create_sequences(features_df, df) if len(X_cont) == 0: return None # 用最后序列预测 last_seq = X_cont[-1:].astype(np.float32) last_cat = X_cat[-1:].astype(np.int32) preds, attn = self.model.predict(last_seq, last_cat) # 特征重要性分析(用注意力权重) last_features = features_df.iloc[-len(X_cont):] trend_val = preds[0, 0] price_chg = preds[0, 1] volatility = preds[0, 2] if trend_val > 0.15: trend_str = '看涨' confidence = min(trend_val * 50, 95) elif trend_val < -0.15: trend_str = '看跌' confidence = min(abs(trend_val) * 50, 95) else: trend_str = '震荡' confidence = 50 last_close = float(df['close'].iloc[-1]) target_price = last_close * (1 + price_chg) result = { 'trend_direction': trend_str, 'confidence': round(confidence, 1), 'trend_score': round(float(trend_val), 4), 'price_change_pct': round(float(price_chg) * 100, 2), 'target_price': round(float(target_price), 2), 'last_close': round(last_close, 2), 'volatility': round(float(volatility), 4), 'data_points': len(df), 'feature_count': X_cont.shape[2], } return result def get_factor_contributions(self, df, financial_data=None): """分析各因子类别对预测的贡献""" if not self.is_trained or self.model is None: return None features_df = self.feature_engineer.extract_all_features(df, financial_data) X_cont, X_cat, _ = self.feature_engineer.create_sequences(features_df, df) if len(X_cont) == 0: return None last_seq = X_cont[-1:].astype(np.float32) last_cat = X_cat[-1:].astype(np.int32) # 基线预测 base_pred, _ = self.model.predict(last_seq, last_cat) base_trend = base_pred[0, 0] # 逐类因子消融 info = self.feature_info['categories'] contributions = {} offset = 0 for cat_name, cat_features in info.items(): n_cat = len(cat_features) if offset + n_cat > last_seq.shape[2]: offset += n_cat continue # 置零该类因子 ablated = last_seq.copy() ablated[:, :, offset:offset + n_cat] = 0 ablated_pred, _ = self.model.predict(ablated, last_cat) contribution = base_trend - ablated_pred[0, 0] contributions[cat_name] = { 'contribution': round(float(contribution), 4), 'n_features': n_cat, 'importance_pct': 0, # 后面归一化 } offset += n_cat # 归一化 total = sum(abs(v['contribution']) for v in contributions.values()) + 1e-10 for v in contributions.values(): v['importance_pct'] = round(abs(v['contribution']) / total * 100, 1) return contributions def save_report(self): """保存训练报告""" if not self.train_report: return None os.makedirs(REPORT_DIR, exist_ok=True) report_path = os.path.join(REPORT_DIR, 'multifactor_report.json') report = self.train_report.copy() report['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') with open(report_path, 'w', encoding='utf-8') as f: json.dump(report, f, indent=2, ensure_ascii=False, default=str) return report_path