Files
sanguo_quant_live/strategies/factors-dynamic-weight-timing-20260327/main_strategy.py
T
2026-03-28 00:14:34 +08:00

408 lines
15 KiB
Python

"""
进阶多因子+动态加权+估值择时 A股量化中低频策略
主策略实现,遵循vn.py CtaStrategy接口
"""
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, TickData
# 导入我们的因子和工具
import sys
sys.path.append('..')
from factors import (
PEFactor, PBFactor, ROEFactor,
Momentum1MFactor, Momentum3MFactor,
VolatilityFactor, SizeFactor,
SectorStrengthFactor
)
from utils import FactorCombiner, DynamicWeightAdjuster, MarketValuationTiming
class MultiFactorDynamicStrategy(CtaTemplate):
"""
进阶多因子策略
特点:
1. 多因子复合选股
2. 动态加权(根据IC调整)
3. 估值择时(调整整体仓位)
4. 中低频调仓(月频/周频)
"""
# 策略参数
author = "翼德"
parameters = [
"rebalance_freq", # 调仓频率,'M'月频 'W'周频
"holding_size", # 持股数量
"top_select", # 选前N%的股票
"dynamic_weight", # 是否开启动态加权
"ic_window", # IC观测窗口
"market_timing", # 是否开启估值择时
"min_position", # 最小仓位
"max_position", # 最大仓位
"max_sector_pct", # 单板块最大仓位占比(相对于总仓位)
]
# 策略变量
variables = [
"current_factor_scores", # 当前因子得分
"current_weights", # 当前因子权重
"target_position", # 当前目标仓位
"last_rebalance_date", # 上次调仓日期
"max_sector_pct", # 单板块最大仓位
]
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.holding_size = getattr(self, 'holding_size', 50) # 默认持有50只
self.top_select = getattr(self, 'top_select', 0.1) # 选前10%
self.dynamic_weight = getattr(self, 'dynamic_weight', True)
self.ic_window = getattr(self, 'ic_window', 12)
self.market_timing = getattr(self, 'market_timing', True)
self.min_position = getattr(self, 'min_position', 0.3)
self.max_position = getattr(self, 'max_position', 1.0)
self.max_sector_pct = getattr(self, 'max_sector_pct', 0.20) # 单板块最大20%仓位
# 初始化因子
self._init_factors()
# 初始化因子合成器
# 调整后的初始权重(适配结构化行情):
# 总权重100%,趋势因子从15%→20%,新增板块强度10%
default_weights = {
'pe': 0.15,
'pb': 0.15,
'roe': 0.15,
'momentum_1m': 0.08,
'momentum_3m': 0.12, # 趋势合计 0.08+0.12=0.20 → 20%
'volatility_3m': 0.15,
'size': 0.15,
'sector_strength': 0.10, # 新增板块强度 10%
}
# 检查是否所有因子都覆盖
factor_dict = {f.name: f for f in self.factors_list}
self.factor_combiner = FactorCombiner(factor_dict, default_weights)
# 初始化动态加权
if self.dynamic_weight:
factor_names = list(factor_dict.keys())
self.dynamic_adjuster = DynamicWeightAdjuster(
factor_names,
window_size=self.ic_window
)
# 初始化估值择时
if self.market_timing:
self.market_timer = MarketValuationTiming(
min_position=self.min_position,
max_position=self.max_position
)
# 策略状态变量
self.current_factor_scores = None
self.current_weights = self.factor_combiner.get_weights()
self.target_position = self.max_position # 默认最大仓位
self.last_rebalance_date = None
def _init_factors(self):
"""初始化所有因子列表
调整后权重(适配结构化行情):
- PE: ~15%
- PB: ~15%
- ROE: ~15%
- Momentum1M: 10% → 提高到 ~12%
- Momentum3M: 10% → 提高到 ~13% (合计趋势因子20%)
- Volatility: ~15%
- Size: ~15%
- SectorStrength: +10%
"""
self.factors_list = [
PEFactor(),
PBFactor(),
ROEFactor(),
Momentum1MFactor(),
Momentum3MFactor(),
VolatilityFactor(),
SizeFactor(),
SectorStrengthFactor() # 新增板块强度因子,适配结构化行情
]
def on_init(self):
"""策略初始化"""
self.write_log("策略初始化完成")
self.load_bar(1000) # 加载1000天数据用于热身
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()
# 更新上次调仓日期
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':
# 周频调仓:不同周就调仓
current_week = current_date.isocalendar()[1]
last_week = self.last_rebalance_date.isocalendar()[1]
if current_week != last_week:
return True
return False
def calculate_factors(self, data: pd.DataFrame) -> pd.Series:
"""
计算所有因子,合成最终得分
参数:
data: 所有股票的行情财务数据,columns需要包含因子所需列
返回:
最终得分,降序排列
"""
# 合成因子得分
final_scores = self.factor_combiner.combine(data)
# 排序,得分高在前
final_scores = final_scores.sort_values(ascending=False)
return final_scores
def select_stocks(self, scores: pd.Series) -> List:
"""
根据因子得分选股票
参数:
scores: 因子得分降序排列
返回:
选中的股票列表
"""
n_total = len(scores.dropna())
if self.holding_size:
# 固定持股数量
n_select = self.holding_size
else:
# 按比例选
n_select = int(n_total * self.top_select)
# 选前n个
selected = scores.head(n_select).index.tolist()
return selected
def calculate_weights_for_selected(self, selected: List, data: pd.DataFrame) -> Dict[str, float]:
"""
计算选中股票的目标权重
考虑整体择时仓位
考虑板块限制:单板块最高不超过max_sector_pct
等权分配给选中股票,然后调整板块超限
"""
n = len(selected)
if n == 0:
return {}
# 获取选中股票的板块信息
selected_df = data.loc[data['symbol'].isin(selected)]
# 初步等权分配
single_weight = self.target_position / n
weights = {symbol: single_weight for symbol in selected}
# 检查板块仓位,超过限制就平均压缩
# 按板块汇总
if 'sector' in selected_df.columns:
sector_weights = {}
for symbol in selected:
sector = selected_df[selected_df['symbol'] == symbol]['sector'].iloc[0]
w = weights[symbol]
if sector not in sector_weights:
sector_weights[sector] = 0
sector_weights[sector] += w
# 检查超限
max_allowed = self.target_position * self.max_sector_pct
for sector, total_w in sector_weights.items():
if total_w > max_allowed:
# 需要压缩,计算压缩比例
ratio = max_allowed / total_w
# 压缩该板块所有股票
for symbol in selected:
s = selected_df[selected_df['symbol'] == symbol]['sector'].iloc[0]
if s == sector:
weights[symbol] *= ratio
# 重新归一化,保证总权重还是target_position
total = sum(weights.values())
if total > 0:
ratio = self.target_position / total
for symbol in weights:
weights[symbol] *= ratio
return weights
def update_dynamic_weights(self, data: pd.DataFrame, forward_returns: pd.Series):
"""
更新动态权重(基于IC)
"""
if not self.dynamic_weight:
return
# 计算每个因子当期IC并更新历史
# 获取当前因子得分
factor_scores = {}
for name, factor in self.factor_combiner.factors.items():
factor_scores[name] = factor.process(data)
factor_df = pd.DataFrame(factor_scores)
self.dynamic_adjuster.update_ic(factor_df, forward_returns)
# 计算新权重
new_weights = self.dynamic_adjuster.calculate_weights()
self.factor_combiner.update_weights(new_weights)
self.current_weights = new_weights
self.write_log(f"动态权重更新完成: {new_weights}")
def update_market_timing(self, market_pe: float):
"""
更新估值择位
"""
if not self.market_timing:
return
self.market_timer.update_valuation(market_pe)
self.target_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_position:.2f}")
def rebalance(self):
"""
执行调仓
这是主调仓逻辑,实际回测中vn.py会调用这里
"""
# 获取最新数据(实际使用中从数据接口获取)
# 这里留出接口,实际回测时填充
data = self.get_current_market_data()
if data is None or len(data) == 0:
self.write_log("没有可用数据,跳过调仓")
return
# 如果有动态加权,更新权重
if self.dynamic_weight and hasattr(self, 'last_data'):
# 计算上期到这期的收益率
forward_returns = self.calculate_forward_returns(self.last_data, data)
self.update_dynamic_weights(self.last_data, forward_returns)
# 计算因子得分
scores = self.calculate_factors(data)
# 选股
selected = self.select_stocks(scores)
# 计算权重(包含择时仓位,包含板块限制)
target_weights = self.calculate_weights_for_selected(selected, data)
# 执行调仓(调用vn.py接口)
self.rebalance_portfolio(target_weights)
# 保存数据供下次动态加权使用
self.last_data = data
self.write_log(f"调仓完成,选中{len(selected)}只股票,目标仓位{self.target_position:.2f}")
def get_current_market_data(self) -> Optional[pd.DataFrame]:
"""
获取当前市场数据(包含收盘价、财务指标等)
需要根据实际数据源实现
这里留出接口
"""
# 实际使用中,从你的数据接口获取
# 返回格式:
# index = (date, symbol) 或者 MultiIndex
# columns 需要包含: pe, pb, roe, close, market_cap 等
return None
def calculate_forward_returns(self, last_data: pd.DataFrame, current_data: pd.DataFrame) -> pd.Series:
"""计算远期收益率(用于IC计算)"""
# 获取价格计算收益率
last_close = last_data.groupby(level='symbol')['close'].last()
current_close = current_data.groupby(level='symbol')['close'].last()
forward_returns = (current_close - last_close) / last_close
return forward_returns
def rebalance_portfolio(self, target_weights: Dict[str, float]):
"""
实际执行调仓,调整每个股票仓位
"""
# 获取当前持仓
current_holds = self.get_all_holds()
# 平掉不在目标中的持仓
for symbol in current_holds:
if symbol not in target_weights:
self.sell(symbol, current_holds[symbol].price, 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股最小买100股
target_volume = int(target_value / current_price / 100) * 100
if target_volume <= 0:
continue
# 获取当前持仓
current_hold = current_holds.get(symbol, None)
current_volume = current_hold.volume if current_hold 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()