""" 结构化适配动态多因子策略 完整方案: 1. 因子池: 价值20% + 质量20% + 成长15% + 中国特色25% + 技术趋势10% + 板块强度10% 2. 因子预处理: 行业中性 + 市值中性 + zscore标准化 3. 动态加权: 每月更新IC,IC越高权重越高 4. 选股: top20-30,只保留站上年线的 5. 仓位: 估值择时定总仓位,单票不超5%,单板块不超20% """ import pandas as pd import numpy as np from typing import Dict, List, Optional from vnpy.trader.app.ctaStrategy import CtaTemplate from vnpy.trader.object import BarData # 导入因子和工具 import sys sys.path.append('.') from factors import ( PEFactor, PBFactor, ROEFactor, RevenueGrowthFactor, ProfitGrowthFactor, SizeFactor, ReversalFactor, Momentum1MFactor, Momentum3MFactor, SectorStrengthFactor ) from utils import ( batch_process_factors, prepare_market_cap_neutral, DynamicICWeightAdjuster, MarketValuationTiming ) from data import ( FactorDataProcessor, SectorStrengthEngine ) from strategies import ( TechnicalTimingSignals, MarketRegimeWeightAllocator ) class StructuredDynamicMultiFactorStrategy(CtaTemplate): """ 结构化适配动态多因子策略 适配当前A股结构化板块轮动行情 """ # 策略参数 - 全部开放配置,方便参数遍历优化 author = "翼德" parameters = [ # 调仓设置 "rebalance_freq", # 调仓频率: 'M'月频, 'W'周频 "top_n", # 选股数量 # 过滤条件 "require_above_ma", # 是否要求站上年线 "ma_period", # 年线周期 # 因子权重(初始权重,动态加权会更新) "value_weight", # 价值因子权重(PE+PB) "quality_weight", # 质量因子权重(ROE) "growth_weight", # 成长因子权重(增速) "china_special_weight", # 中国特色权重(市值+反转) "trend_weight", # 趋势因子权重(动量) "sector_strength_weight", # 板块强度权重 # 板块强度计算权重 "sector_tech_weight", # 板块强度技术面权重 "sector_fund_weight", # 板块强度资金面权重 "sector_basic_weight", # 板块强度基本面权重 "sector_sentiment_weight", # 板块强度情绪面权重 # 仓位限制 "max_single_pct", # 单票最大仓位占比 "max_sector_pct", # 单板块最大仓位占比(相对于总仓位) # 动态加权设置 "dynamic_weight_enabled", # 是否开启动态加权 "ic_window", # IC滚动窗口月数 # 市场状态权重 "regime_weight_enabled", # 是否开启牛熊市不同权重分配 # 估值择时设置 "market_timing_enabled", # 是否开启估值择时 "min_total_position", # 最小总仓位 "max_total_position", # 最大总仓位 # 技术择时过滤 "tech_filter_enabled", # 是否开启技术择时二次过滤 "min_bullish_score", # 最小看多得分才能入选 ] # 策略变量 variables = [ "current_weights", # 当前因子权重 "target_total_position", # 当前目标总仓位 "last_rebalance_date", # 上次调仓日期 "current_ic", # 当前各因子IC ] def __init__(self, cta_engine, strategy_name, setting_dict): super().__init__(cta_engine, strategy_name, setting_dict) # ========== 默认参数 ========== # 调仓设置 self.rebalance_freq = getattr(self, 'rebalance_freq', 'M') self.top_n = getattr(self, 'top_n', 25) # 过滤条件 self.require_above_ma = getattr(self, 'require_above_ma', True) self.ma_period = getattr(self, 'ma_period', 250) # 因子初始权重 self.value_weight = getattr(self, 'value_weight', 0.20) self.quality_weight = getattr(self, 'quality_weight', 0.20) self.growth_weight = getattr(self, 'growth_weight', 0.15) self.china_special_weight = getattr(self, 'china_special_weight', 0.25) self.trend_weight = getattr(self, 'trend_weight', 0.10) self.sector_strength_weight = getattr(self, 'sector_strength_weight', 0.10) # 板块强度计算权重 self.sector_tech_weight = getattr(self, 'sector_tech_weight', 0.40) self.sector_fund_weight = getattr(self, 'sector_fund_weight', 0.30) self.sector_basic_weight = getattr(self, 'sector_basic_weight', 0.20) self.sector_sentiment_weight = getattr(self, 'sector_sentiment_weight', 0.10) # 仓位限制 self.max_single_pct = getattr(self, 'max_single_pct', 0.05) self.max_sector_pct = getattr(self, 'max_sector_pct', 0.20) # 动态加权 self.dynamic_weight_enabled = getattr(self, 'dynamic_weight_enabled', True) self.ic_window = getattr(self, 'ic_window', 12) # 市场状态权重 self.regime_weight_enabled = getattr(self, 'regime_weight_enabled', True) # 估值择时 self.market_timing_enabled = getattr(self, 'market_timing_enabled', True) self.min_total_position = getattr(self, 'min_total_position', 0.3) self.max_total_position = getattr(self, 'max_total_position', 1.0) # 技术择时二次过滤 self.tech_filter_enabled = getattr(self, 'tech_filter_enabled', True) self.min_bullish_score = getattr(self, 'min_bullish_score', 0.4) # ========== 初始化因子 ========== self.factors = self._init_factors() # ========== 初始化板块强度引擎 ========== self.sector_strength_engine = SectorStrengthEngine( weight_tech=self.sector_tech_weight, weight_fund=self.sector_fund_weight, weight_basic=self.sector_basic_weight, weight_sentiment=self.sector_sentiment_weight ) # ========== 初始化技术择时 ========== self.tech_timing = TechnicalTimingSignals() # ========== 初始化市场状态权重 ========== if self.regime_weight_enabled: self.regime_allocator = MarketRegimeWeightAllocator() # 根据当前市场状态获取权重 index_data = self.get_market_index_data() if index_data is not None: default_weights = self.regime_allocator.expand_to_factor_names( self.regime_allocator.get_weights(index_data) ) else: # 默认震荡市权重 default_weights = self.regime_allocator.expand_to_factor_names( self.regime_allocator.weights['range_bound'] ) else: # 固定权重按配置比例分配 # 价值类两个因子平分value_weight # 成长类两个因子平分growth_weight # 中国特色两个因子平分china_special_weight # 趋势两个因子平分trend_weight default_weights = { 'pe': self.value_weight * 0.5, 'pb': self.value_weight * 0.5, 'roe': self.quality_weight, 'revenue_growth': self.growth_weight * 0.5, 'profit_growth': self.growth_weight * 0.5, 'size': self.china_special_weight * 0.5, 'reversal_1m': self.china_special_weight * 0.5, 'momentum_1m': self.trend_weight * 0.3, 'momentum_3m': self.trend_weight * 0.7, 'sector_strength': self.sector_strength_weight, } # 因子字典 {名称: 实例} self.factor_dict = {f.name: f for f in self.factors} # ========== 初始化动态加权 ========== if self.dynamic_weight_enabled: factor_names = list(self.factor_dict.keys()) self.ic_adjuster = DynamicICWeightAdjuster( factor_names, window_size=self.ic_window ) # ========== 初始化估值择时 ========== if self.market_timing_enabled: self.market_timer = MarketValuationTiming( min_position=self.min_total_position, max_position=self.max_total_position ) self.target_total_position = self.max_total_position else: self.target_total_position = self.max_total_position # 状态变量 self.current_weights = default_weights self.last_rebalance_date = None self.current_ic = None self.last_data = None # 保存上期数据用于IC计算 def _init_factors(self): """初始化所有因子实例""" factors = [ PEFactor(), PBFactor(), ROEFactor(), RevenueGrowthFactor(), ProfitGrowthFactor(), SizeFactor(), ReversalFactor(), Momentum1MFactor(), Momentum3MFactor(), SectorStrengthFactor(), ] return factors def on_init(self): """策略初始化""" self.write_log( f"结构化动态多因子初始化完成: " f"选股{self.top_n}只,单票{self.max_single_pct*100:.0f}%," f"单板块{self.max_sector_pct*100:.0f}%," f"动态加权={'开' if self.dynamic_weight_enabled else '关'}," f"估值择时={'开' if self.market_timing_enabled else '关'}" ) # 加载足够数据 required_bars = max(self.ma_period, 252) self.load_bar(required_bars) def on_start(self): self.write_log("策略启动") def on_stop(self): self.write_log("策略停止") def on_bar(self, bar: BarData): """K线推送,判断调仓""" current_date = bar.datetime.date() if not self._need_rebalance(current_date): return self.rebalance(current_date) self.last_rebalance_date = current_date def _need_rebalance(self, current_date) -> bool: """判断是否需要调仓""" if self.last_rebalance_date is None: return True if self.rebalance_freq == 'M': # 不同月份调仓 if current_date.month != self.last_rebalance_date.month: return True elif self.rebalance_freq == 'W': # 不同周调仓 curr_week = current_date.isocalendar()[1] last_week = self.last_rebalance_date.isocalendar()[1] if curr_week != last_week: return True return False def _filter_above_ma(self, candidate_symbols: List[str], data: pd.DataFrame) -> List[str]: """过滤出站上年线的股票""" if not self.require_above_ma: return candidate_symbols filtered = [] for symbol in candidate_symbols: # 获取股票数据,计算年线 symbol_data = data[data['symbol'] == symbol] if len(symbol_data) < self.ma_period: continue # 数据不够,跳过 # 计算年线 ma = symbol_data['close'].rolling(self.ma_period).mean().iloc[-1] current_close = symbol_data['close'].iloc[-1] if current_close > ma: filtered.append(symbol) return filtered def calculate_total_score(self, processed_factors: pd.DataFrame) -> pd.Series: """根据因子权重计算总得分""" total_score = None for name, weight in self.current_weights.items(): if name not in processed_factors.columns: continue score = processed_factors[name] * weight if total_score is None: total_score = score else: total_score = total_score.add(score, fill_value=0) # 降序排列,得分越高越好 if total_score is not None: total_score = total_score.sort_values(ascending=False) return total_score def select_stocks(self, total_score: pd.Series, data: pd.DataFrame) -> List[str]: """选股: 1. 因子打分选topN*2 2. 年线过滤(如果开启) 3. 技术择时二次过滤(如果开启),只保留多头信号够的 """ # 选topN*2得分最高 top_candidates = total_score.head(self.top_n * 3).index.tolist() # 多选一些给两轮过滤 # 第一步过滤站上年线 if self.require_above_ma: filtered = self._filter_above_ma(top_candidates, data) else: filtered = top_candidates # 第二步技术择时二次过滤 if self.tech_filter_enabled and len(filtered) > 0: # 拿到每个股票的K线数据 symbol_data = {} for symbol in filtered: symbol_df = data[data['symbol'] == symbol].sort_values('date') if len(symbol_df) > self.ma_period: symbol_data[symbol] = symbol_df filtered = self.tech_timing.filter_stocks_by_timing( symbol_data, min_bullish_score=self.min_bullish_score ) # 如果过滤后太少,放宽再选 if len(filtered) < self.top_n // 2: top_candidates = total_score.head(self.top_n * 4).index.tolist() if self.require_above_ma: filtered = self._filter_above_ma(top_candidates, data) else: filtered = top_candidates if self.tech_filter_enabled and len(filtered) > 0: symbol_data = {} for symbol in filtered: symbol_df = data[data['symbol'] == symbol].sort_values('date') symbol_data[symbol] = symbol_df filtered = self.tech_timing.filter_stocks_by_timing( symbol_data, min_bullish_score=self.min_bullish_score ) # 保证不超过topN selected = filtered[:self.top_n] return selected def calculate_target_weights( self, selected: List[str], data: pd.DataFrame ) -> Dict[str, float]: """计算目标权重,考虑: 1. 整体目标仓位 2. 单票不超max_single_pct 3. 单板块不超max_sector_pct """ n = len(selected) if n == 0: return {} # 初步等权分配,考虑单票最大限制 max_single_allowed = self.target_total_position * self.max_single_pct equal_weight = self.target_total_position / n # 如果等权已经超过单票最大,按最大单票 if equal_weight > max_single_allowed: equal_weight = max_single_allowed # 初步分配 weights = {symbol: equal_weight for symbol in selected} # 检查板块限制 if 'sector' in data.columns: # 获取选中股票板块 selected_data = data[data['symbol'].isin(selected)] # 按板块汇总 sector_total = {} for symbol in selected: row = selected_data[selected_data['symbol'] == symbol] sector = row['sector'].iloc[0] if sector not in sector_total: sector_total[sector] = 0 sector_total[sector] += weights[symbol] # 检查超限,压缩 max_allowed = self.target_total_position * self.max_sector_pct for sector, total_w in sector_total.items(): if total_w > max_allowed: # 压缩比例 ratio = max_allowed / total_w # 压缩该板块所有股票 for symbol in selected: s_row = selected_data[selected_data['symbol'] == symbol] s_sector = s_row['sector'].iloc[0] if s_sector == sector: weights[symbol] *= ratio # 重新归一,保证总权重还是target_total_position total = sum(weights.values()) if total > 0: ratio = self.target_total_position / total for symbol in weights: weights[symbol] *= ratio return weights def update_dynamic_weights(self, last_data: pd.DataFrame, current_data: pd.DataFrame): """更新动态权重(基于IC)""" if not self.dynamic_weight_enabled: return # 计算远期收益率 last_close = last_data.groupby('symbol')['close'].last() current_close = current_data.groupby('symbol')['close'].last forward_returns = (current_close - last_close) / last_close # 处理因子得到当前因子得分 prepared_data = prepare_market_cap_neutral(current_data) factor_df = batch_process_factors(self.factor_dict, prepared_data) # 更新IC self.current_ic = self.ic_adjuster.update_monthly_ic(factor_df, forward_returns) # 计算新权重 new_weights = self.ic_adjuster.calculate_weights() self.current_weights = new_weights self.write_log(f"动态权重已更新,当前IC: {self.current_ic}") def update_market_timing(self, market_pe: float): """更新估值择位""" if not self.market_timing_enabled: return self.market_timer.update_monthly(market_pe) self.target_total_position = self.market_timer.calculate_target_position() q = self.market_timer.get_current_quantile() if q is not None: self.write_log( f"估值择时更新: 分位数={q:.2f}, 目标仓位={self.target_total_position:.2f}" ) def rebalance(self, current_date): """执行调仓""" # 获取最新全市场数据 data = self.get_current_market_data() if data is None or len(data) == 0: self.write_log("没有数据,跳过调仓") return # 预处理:添加log市值用于中性化 data = prepare_market_cap_neutral(data) # 如果开启市场状态权重分配,根据当前市场更新因子权重 if self.regime_weight_enabled: index_data = self.get_market_index_data() if index_data is not None: new_weights = self.regime_allocator.get_weights(index_data) expanded_weights = self.regime_allocator.expand_to_factor_names(new_weights) self.current_weights = expanded_weights current_regime = self.regime_allocator.classify_market_regime(index_data) self.write_log(f"市场状态识别: {current_regime}, 权重已更新") # 如果有上期数据,更新动态IC权重 if self.dynamic_weight_enabled and self.last_data is not None: self.update_dynamic_weights(self.last_data, data) # 如果开启择时,更新整体仓位 if self.market_timing_enabled: market_pe = self.calculate_market_pe(data) self.update_market_timing(market_pe) # 批量处理所有因子(中性化+标准化+rank) processed_factors = batch_process_factors(self.factor_dict, data) # 计算总得分 total_score = self.calculate_total_score(processed_factors) if total_score is None or len(total_score.dropna()) == 0: self.write_log("计算得分失败,跳过调仓") return # 选股 selected = self.select_stocks(total_score, data) if len(selected) == 0: self.write_log("没有选中股票,跳过调仓") return # 计算目标权重(考虑板块和单票限制) target_weights = self.calculate_target_weights(selected, data) # 执行调仓 self.execute_rebalance(target_weights) # 保存数据供下次IC计算 self.last_data = data self.write_log( f"调仓完成: 日期{current_date}, 选中{len(selected)}只股票, " f"目标总仓位{self.target_total_position:.2f}" ) def get_current_market_data(self) -> Optional[pd.DataFrame]: """ 获取当前全市场数据 需要包含列: - symbol: 股票代码 - date: 日期 - close: 收盘价 - pe: 市盈率 - pb: 市净率 - roe: 净资产收益率 - revenue_yoy: 营收同比增速 - profit_yoy: 利润同比增速 - market_cap: 市值 - sector: 板块 - industry_code: 行业代码(中性化用) """ # 这里需要对接你的数据源,接口留出 return None def calculate_market_pe(self, data: pd.DataFrame) -> float: """计算全市场平均PE用于估值择时""" pes = data['pe'].dropna() pes = pes[pes > 0] if len(pes) == 0: return 15 # 默认中性值 # 中位数PE return pes.median() def execute_rebalance(self, target_weights: Dict[str, float]): """实际执行调仓""" # 获取当前持仓 current_positions = self.get_all_positions() # 平掉不在目标中的持仓 for pos in current_positions: symbol = pos.vt_symbol if symbol not in target_weights and pos.volume > 0: self.sell(symbol, self.get_last_price(symbol), 0) # 调整目标持仓 for symbol, target_weight in target_weights.items(): target_value = self.balance * target_weight current_price = self.get_last_price(symbol) if current_price <= 0: continue # A股整手 target_volume = int(target_value / current_price / 100) * 100 if target_volume <= 0: continue # 获取当前持仓 current_pos = self.get_position(symbol) current_volume = current_pos.volume if current_pos else 0 if target_volume > current_volume: # 加仓 volume = target_volume - current_volume self.buy(symbol, current_price, volume) elif target_volume < current_volume: # 减仓 volume = current_volume - target_volume self.sell(symbol, current_price, volume) self.put_order()