542 lines
20 KiB
Python
542 lines
20 KiB
Python
"""
|
||
结构化适配动态多因子 - 风控和择时模块
|
||
|
||
功能:
|
||
1. 个股风险评估:五维风险 + 消息提前监控 + 国际联动风险
|
||
2. 择时适配结构化:板块轮动择时,仓位控制,热度控制
|
||
3. 风控适配结构化:板块风格集中度控制,热点灵敏度提高
|
||
4. 全参数抽象化:所有阈值可配置,方便参数优化
|
||
|
||
Author: 关羽(云长)
|
||
Date: 2026-03-27
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
from typing import List, Dict, Optional, Tuple
|
||
from enum import Enum
|
||
|
||
|
||
class RiskLevel(Enum):
|
||
SAFE = 0
|
||
WATCH = 1
|
||
REDUCE = 2
|
||
EXIT = 3
|
||
|
||
|
||
class Sentiment(Enum):
|
||
STRONG_BULL = 2
|
||
BULL = 1
|
||
NEUTRAL = 0
|
||
BEAR = -1
|
||
STRONG_BEAR = -2
|
||
|
||
|
||
# ==================== 数据结构 ====================
|
||
|
||
@dataclass
|
||
class IndividualStockRisk:
|
||
"""单个股风险输入数据"""
|
||
code: str
|
||
name: str
|
||
|
||
# 五维基础风险
|
||
volatility: float # 波动率风险 0~1
|
||
fundamental: float # 基本面风险 0~1
|
||
liquidity: float # 流动性风险 0~1
|
||
valuation: float # 估值风险 0~1
|
||
|
||
# 消息监控维度
|
||
recent_vol_change: float # 成交量变化相对20日均值
|
||
recent_pct_change: float # 近5日涨跌幅%
|
||
gap_down: bool = False
|
||
finance_balance_change: float = 0.0 # 融资余额周变化
|
||
has_inst_sell: bool = False # 龙虎榜机构卖出
|
||
has_bulk_discount: bool = False
|
||
bulk_discount: float = 0.0
|
||
discussion_change: float = 0.0 # 讨论量变化倍数
|
||
sentiment_score: float = 0.0 # -1~1
|
||
|
||
# 国际联动
|
||
is_ah: bool = False
|
||
ah_hk_change: float = 0.0
|
||
is_commodity: bool = False
|
||
commodity_future_change: float = 0.0
|
||
us_index_change: float = 0.0
|
||
has_major_intl_news: bool = False
|
||
intl_news_sentiment: int = 0
|
||
|
||
# 结构化板块
|
||
sector_name: str = ""
|
||
sector_style: str = ""
|
||
sector_total_gain: float = 0.0 # 板块近期涨幅%
|
||
sector_position_ratio: float = 0.0 # 板块仓位占比
|
||
|
||
|
||
@dataclass
|
||
class SectorInfo:
|
||
"""板块信息"""
|
||
name: str
|
||
style: str
|
||
recent_gain: float
|
||
position_ratio: float
|
||
stock_count: int
|
||
is_hot: bool = False
|
||
|
||
|
||
@dataclass
|
||
class OverallRiskResult:
|
||
"""整体风险评估结果"""
|
||
total_risk_score: float # 0~1
|
||
risk_level: RiskLevel
|
||
individual_risk: float
|
||
news_risk: float
|
||
sector_risk: float
|
||
suggestion: str
|
||
warnings: List[str]
|
||
|
||
|
||
# ==================== 参数配置 ====================
|
||
|
||
@dataclass
|
||
class RiskConfig:
|
||
"""风控参数配置,全部抽象化,方便优化"""
|
||
|
||
# === 个股消息监控参数 ===
|
||
vol_alert_threshold: float = 2.0 # 放量超过2倍预警
|
||
drop_alert_threshold: float = -7.0 # 5日跌幅超过7%预警
|
||
finance_decrease_threshold: float = -0.2 # 融资减少超过20%预警
|
||
bulk_discount_threshold: float = -0.08 # 大宗折价超过8%预警
|
||
discussion_hot_threshold: float = 5.0 # 讨论量超过5倍预警
|
||
|
||
# === 国际联动参数 ===
|
||
ah_drop_threshold: float = -3.0 # H股跌幅超过3%预警
|
||
commodity_drop_threshold: float = -4.0 # 商品跌幅超过4%预警
|
||
us_drop_threshold: float = -2.0 # 美股跌幅超过2%预警
|
||
|
||
# === 结构化择时参数 ===
|
||
single_sector_high_position: float = 0.15 # 单板块仓位超过提示减仓
|
||
sector_rally_warning: float = 30.0 # 板块涨幅30%提示
|
||
sector_rally_risk: float = 50.0 # 板块涨幅50%提高风险权重
|
||
sector_dump_opportunity: float = -20.0 # 板块跌幅20%提示机会
|
||
|
||
# === 结构化风控参数 ===
|
||
max_single_sector: float = 0.25 # 单板块最大仓位
|
||
max_single_style: float = 0.40 # 单风格最大仓位
|
||
max_hot_total: float = 0.50 # 热点板块总仓位最大
|
||
hot_risk_multiplier: float = 2.0 # 热点板块风险放大倍数
|
||
medium_hot_multiplier: float = 1.5 # 中等热点放大倍数
|
||
|
||
# === 五维风险权重 ===
|
||
weight_volatility: float = 0.15
|
||
weight_fundamental: float = 0.20
|
||
weight_liquidity: float = 0.15
|
||
weight_valuation: float = 0.20
|
||
weight_news: float = 0.30 # 消息风险权重更高
|
||
|
||
# === 风险等级阈值 ===
|
||
threshold_exit: float = 0.7
|
||
threshold_reduce: float = 0.4
|
||
threshold_watch: float = 0.2
|
||
|
||
|
||
# 默认配置
|
||
default_config = RiskConfig()
|
||
|
||
|
||
# ==================== 个股风险评估 ====================
|
||
|
||
class IndividualRiskAssessor:
|
||
"""个股五维风险+消息风险评估"""
|
||
|
||
def __init__(self, config: RiskConfig = None):
|
||
self.config = config or default_config
|
||
|
||
def assess_base_five_dimension(self, stock: IndividualStockRisk) -> float:
|
||
"""计算基础五维风险"""
|
||
base_risk = (
|
||
stock.volatility * self.config.weight_volatility +
|
||
stock.fundamental * self.config.weight_fundamental +
|
||
stock.liquidity * self.config.weight_liquidity +
|
||
stock.valuation * self.config.weight_valuation
|
||
)
|
||
return base_risk
|
||
|
||
def assess_news_risk(self, stock: IndividualStockRisk) -> Tuple[float, List[str]]:
|
||
"""计算消息面风险,返回风险分0~1和警告列表"""
|
||
risk_points = 0
|
||
max_points = 100
|
||
warnings = []
|
||
|
||
# 1. 量价异常
|
||
if stock.recent_vol_change >= self.config.vol_alert_threshold and stock.recent_pct_change >= 5:
|
||
risk_points += 15
|
||
warnings.append(f"近5日放量{stock.recent_vol_change:.1f}倍涨幅{stock.recent_pct_change:.1f}%,警惕利好出货")
|
||
|
||
if stock.recent_pct_change <= self.config.drop_alert_threshold and stock.recent_vol_change >= 0.5:
|
||
risk_points += 20
|
||
warnings.append(f"近5日放量下跌{stock.recent_pct_change:.1f}%,警惕利空提前泄露")
|
||
|
||
if stock.gap_down:
|
||
risk_points += 10
|
||
warnings.append("向下跳空缺口未回补,技术形态偏空")
|
||
|
||
# 2. 融资变化
|
||
if stock.finance_balance_change <= self.config.finance_decrease_threshold:
|
||
risk_points += 15
|
||
warnings.append(f"融资余额一周减少{stock.finance_balance_change:.1%},杠杆资金出逃")
|
||
|
||
# 3. 机构买卖
|
||
if stock.has_inst_sell:
|
||
risk_points += 20
|
||
warnings.append("龙虎榜机构大额卖出")
|
||
|
||
if stock.has_bulk_discount and stock.bulk_discount <= self.config.bulk_discount_threshold:
|
||
risk_points += 15
|
||
warnings.append(f"大宗交易折价{stock.bulk_discount:.1%},大股东出货")
|
||
|
||
# 4. 舆情
|
||
if stock.discussion_change >= self.config.discussion_hot_threshold:
|
||
risk_points += 10
|
||
warnings.append(f"讨论量暴涨{stock.discussion_change:.1f}倍,热度异常")
|
||
|
||
if stock.sentiment_score <= -0.5:
|
||
risk_points += 10
|
||
warnings.append(f"舆情偏空,情感分{stock.sentiment_score:.2f}")
|
||
|
||
# 5. 国际联动
|
||
if stock.is_ah and stock.ah_hk_change <= self.config.ah_drop_threshold:
|
||
risk_points += 15
|
||
warnings.append(f"A+H股,H股隔夜大跌{stock.ah_hk_change:.1f}%")
|
||
|
||
if stock.is_commodity and stock.commodity_future_change <= self.config.commodity_drop_threshold:
|
||
risk_points += 15
|
||
warnings.append(f"大宗商品股,对应期货大跌{stock.commodity_future_change:.1f}%")
|
||
|
||
if stock.us_index_change <= self.config.us_drop_threshold:
|
||
risk_points += 10
|
||
warnings.append(f"美股隔夜大跌{stock.us_index_change:.1f}%,A股承压")
|
||
|
||
if stock.has_major_intl_news and stock.intl_news_sentiment == -1:
|
||
risk_points += 20
|
||
warnings.append("重大国际利空消息,系统性风险上升")
|
||
|
||
# 6. 结构化板块:热点放大风险
|
||
if stock.sector_total_gain >= self.config.sector_rally_risk:
|
||
risk_points = int(risk_points * self.config.hot_risk_multiplier)
|
||
warnings.append(f"板块累计涨幅{stock.sector_total_gain:.1f}%,已是热点,风险放大")
|
||
elif stock.sector_total_gain >= self.config.sector_rally_warning:
|
||
risk_points = int(risk_points * self.config.medium_hot_multiplier)
|
||
warnings.append(f"板块累计涨幅{stock.sector_total_gain:.1f}%,中等热点,风险适度放大")
|
||
|
||
risk_norm = min(risk_points / 50, 1.0)
|
||
return risk_norm, warnings
|
||
|
||
|
||
# ==================== 结构化板块择时风控 ====================
|
||
|
||
class StructuralSectorTiming:
|
||
"""结构化行情板块择时"""
|
||
|
||
def __init__(self, config: RiskConfig = None):
|
||
self.config = config or default_config
|
||
|
||
def timing_single_sector(self, sector: SectorInfo) -> Tuple[str, str, int]:
|
||
"""单板块择时,返回信号、建议、风险增量"""
|
||
risk_inc = 0
|
||
suggestion = ""
|
||
signal = "neutral"
|
||
|
||
# 仓位过高提醒
|
||
if sector.position_ratio > self.config.single_sector_high_position:
|
||
extra = (sector.position_ratio - self.config.single_sector_high_position) * 100
|
||
risk_inc += int(extra)
|
||
suggestion += f"\n⚠️ 单板块仓位{sector.position_ratio:.1%},超过{self.config.single_sector_high_position:.1%},建议减仓分散"
|
||
signal = "bear"
|
||
|
||
# 涨幅过大提醒风险
|
||
if sector.recent_gain >= self.config.sector_rally_risk:
|
||
risk_inc += 20
|
||
suggestion += f"\n🔴 板块累计涨幅{sector.recent_gain:.1f}%,超过{self.config.sector_rally_risk:.0f}%,警惕过热,建议提高止盈标准"
|
||
signal = "bear"
|
||
elif sector.recent_gain >= self.config.sector_rally_warning:
|
||
risk_inc += 10
|
||
suggestion += f"\n🟡 板块累计涨幅{sector.recent_gain:.1f}%,已有较大涨幅,提高风控警惕"
|
||
signal = "bear"
|
||
|
||
# 跌幅过大提示机会
|
||
if sector.recent_gain <= self.config.sector_dump_opportunity and sector.recent_gain >= -40:
|
||
suggestion += f"\n✅ 板块累计跌幅{-sector.recent_gain:.1f}%,如果基本面没问题,可考虑适度低吸布局"
|
||
signal = "bull"
|
||
|
||
if not suggestion:
|
||
suggestion = "✅ 板块仓位和涨幅正常"
|
||
signal = "neutral"
|
||
|
||
return signal, suggestion.strip(), risk_inc
|
||
|
||
|
||
class SectorConcentrationControl:
|
||
"""板块和风格集中度风控"""
|
||
|
||
def __init__(self, config: RiskConfig = None):
|
||
self.config = config or default_config
|
||
|
||
def check_concentration(self, sectors: List[SectorInfo]) -> Tuple[int, List[str]]:
|
||
"""检查集中度风险,返回风险增量和警告"""
|
||
risk_inc = 0
|
||
warnings = []
|
||
|
||
# 单板块检查
|
||
for sector in sectors:
|
||
if sector.position_ratio > self.config.max_single_sector:
|
||
extra = (sector.position_ratio - self.config.max_single_sector) * 100
|
||
risk_inc += int(extra)
|
||
warnings.append(f"⚠️ 板块【{sector.name}】仓位{sector.position_ratio:.1%},超过最大限制{self.config.max_single_sector:.1%}")
|
||
|
||
# 单风格检查
|
||
style_summary: Dict[str, float] = {}
|
||
for sector in sectors:
|
||
style_summary[sector.style] = style_summary.get(sector.style, 0.0) + sector.position_ratio
|
||
|
||
for style, ratio in style_summary.items():
|
||
if ratio > self.config.max_single_style:
|
||
extra = (ratio - self.config.max_single_style) * 50
|
||
risk_inc += int(extra)
|
||
warnings.append(f"⚠️ 风格【{style}】总仓位{ratio:.1%},超过最大限制{self.config.max_single_style:.1%},建议分散配置")
|
||
|
||
# 热点总仓位检查
|
||
hot_total = sum(s.position_ratio for s in sectors if s.is_hot)
|
||
if hot_total > self.config.max_hot_total:
|
||
extra = (hot_total - self.config.max_hot_total) * 80
|
||
risk_inc += int(extra)
|
||
warnings.append(f"⚠️ 所有热点板块合计仓位{hot_total:.1%},超过{self.config.max_hot_total:.1%},总体过热风险")
|
||
|
||
return risk_inc, warnings
|
||
|
||
|
||
# ==================== 总风控控制器 ====================
|
||
|
||
class StructuredDynamicFactorsRiskControl:
|
||
"""结构化动态多因子总风控控制器"""
|
||
|
||
def __init__(self, config: RiskConfig = None):
|
||
self.config = config or default_config
|
||
self.individual_assessor = IndividualRiskAssessor(config)
|
||
self.sector_timing = StructuralSectorTiming(config)
|
||
self.concentration_ctrl = SectorConcentrationControl(config)
|
||
|
||
def assess_stock(self, stock: IndividualStockRisk) -> dict:
|
||
"""评估单个股风险"""
|
||
# 基础五维
|
||
base_risk = self.individual_assessor.assess_base_five_dimension(stock)
|
||
# 消息风险
|
||
news_risk, news_warnings = self.individual_assessor.assess_news_risk(stock)
|
||
# 综合计算
|
||
total_risk = (
|
||
base_risk * (1 - self.config.weight_news) +
|
||
news_risk * self.config.weight_news
|
||
)
|
||
|
||
# 风险等级
|
||
if total_risk >= self.config.threshold_exit:
|
||
level = RiskLevel.EXIT
|
||
suggestion = "建议清仓离场"
|
||
elif total_risk >= self.config.threshold_reduce:
|
||
level = RiskLevel.REDUCE
|
||
suggestion = "建议减仓控制风险"
|
||
elif total_risk >= self.config.threshold_watch:
|
||
level = RiskLevel.WATCH
|
||
suggestion = "继续观察,不新开仓"
|
||
else:
|
||
level = RiskLevel.SAFE
|
||
suggestion = "安全,按计划操作"
|
||
|
||
return {
|
||
"code": stock.code,
|
||
"name": stock.name,
|
||
"total_risk": total_risk,
|
||
"base_risk": base_risk,
|
||
"news_risk": news_risk,
|
||
"risk_level": level,
|
||
"suggestion": suggestion,
|
||
"warnings": news_warnings
|
||
}
|
||
|
||
def assess_portfolio(self,
|
||
stocks: List[IndividualStockRisk],
|
||
sectors: List[SectorInfo]) -> OverallRiskResult:
|
||
"""评估整个组合风险"""
|
||
# 1. 个股平均风险
|
||
if stocks:
|
||
total_individual = sum(self.assess_stock(s)['total_risk'] for s in stocks)
|
||
avg_individual = total_individual / len(stocks)
|
||
else:
|
||
avg_individual = 0.0
|
||
|
||
# 2. 板块风险
|
||
sector_risk_inc, sector_warnings = self.concentration_ctrl.check_concentration(sectors)
|
||
for sector in sectors:
|
||
_, _, inc = self.sector_timing.timing_single_sector(sector)
|
||
sector_risk_inc += inc
|
||
|
||
sector_risk = min(sector_risk_inc / 50, 1.0)
|
||
|
||
# 3. 消息风险平均
|
||
total_news = 0
|
||
all_warnings = []
|
||
for stock in stocks:
|
||
nr, w = self.individual_assessor.assess_news_risk(stock)
|
||
total_news += nr
|
||
all_warnings.extend(w)
|
||
avg_news = total_news / len(stocks) if stocks else 0.0
|
||
|
||
# 4. 整体综合风险
|
||
total_score = (avg_individual * 0.6) + (sector_risk * 0.4)
|
||
total_score = min(total_score, 1.0)
|
||
|
||
# 风险等级
|
||
if total_score >= self.config.threshold_exit:
|
||
level = RiskLevel.EXIT
|
||
suggestion = "整体风险高,建议清仓休息"
|
||
elif total_score >= self.config.threshold_reduce:
|
||
level = RiskLevel.REDUCE
|
||
suggestion = "整体风险中等偏高,建议减仓"
|
||
elif total_score >= self.config.threshold_watch:
|
||
level = RiskLevel.WATCH
|
||
suggestion = "存在一定风险,密切观察"
|
||
else:
|
||
level = RiskLevel.SAFE
|
||
suggestion = "整体风险正常,按计划操作"
|
||
|
||
all_warnings.extend(sector_warnings)
|
||
|
||
return OverallRiskResult(
|
||
total_risk_score=total_score,
|
||
risk_level=level,
|
||
individual_risk=avg_individual,
|
||
news_risk=avg_news,
|
||
sector_risk=sector_risk,
|
||
suggestion=suggestion,
|
||
warnings=all_warnings
|
||
)
|
||
|
||
def get_risk_report(self, result: OverallRiskResult) -> str:
|
||
"""生成风控报告"""
|
||
levels = {
|
||
RiskLevel.SAFE: "🟢 安全",
|
||
RiskLevel.WATCH: "🟡 关注",
|
||
RiskLevel.REDUCE: "🟠 减仓",
|
||
RiskLevel.EXIT: "🔴 离场",
|
||
}
|
||
|
||
lines = []
|
||
lines.append("=" * 60)
|
||
lines.append("结构化动态多因子 组合风控报告")
|
||
lines.append("=" * 60)
|
||
lines.append(f"总体风险评分: {result.total_risk_score:.2f}")
|
||
lines.append(f"风险等级: {levels[result.risk_level]}")
|
||
lines.append(f"个股平均风险: {result.individual_risk:.2f}")
|
||
lines.append(f"消息平均风险: {result.news_risk:.2f}")
|
||
lines.append(f"板块结构风险: {result.sector_risk:.2f}")
|
||
lines.append("")
|
||
|
||
if result.warnings:
|
||
lines.append("⚠️ 风险警告:")
|
||
for w in result.warnings:
|
||
lines.append(f" • {w}")
|
||
lines.append("")
|
||
|
||
lines.append(f"💡 操作建议: {result.suggestion}")
|
||
lines.append("=" * 60)
|
||
return "\n".join(lines)
|
||
|
||
def get_sector_timing_report(self, sectors: List[SectorInfo]) -> str:
|
||
"""生成板块择时报告"""
|
||
lines = []
|
||
lines.append("=" * 60)
|
||
lines.append("结构化板块择时报告")
|
||
lines.append("=" * 60)
|
||
|
||
total_risk = 0
|
||
for sector in sectors:
|
||
signal, suggestion, inc = self.sector_timing.timing_single_sector(sector)
|
||
total_risk += inc
|
||
lines.append(f"【{sector.name}】")
|
||
lines.append(f" 仓位: {sector.position_ratio:.1%} 近期涨幅: {sector.recent_gain:.1f}%")
|
||
lines.append(f" {suggestion}")
|
||
lines.append("")
|
||
|
||
lines.append(f"总风险增量: {total_risk}")
|
||
if total_risk >= 30:
|
||
lines.append("整体结论:板块风险较高,建议减仓分散")
|
||
elif total_risk >= 10:
|
||
lines.append("整体结论:存在一定板块风险,建议密切观察")
|
||
else:
|
||
lines.append("整体结论:板块结构正常")
|
||
|
||
lines.append("=" * 60)
|
||
return "\n".join(lines)
|
||
|
||
|
||
# ==================== 测试 ====================
|
||
|
||
if __name__ == "__main__":
|
||
print("=== 测试结构化动态多因子风控模块 ===\n")
|
||
|
||
# 测试1: 单个股评估
|
||
config = default_config
|
||
rc = StructuredDynamicFactorsRiskControl(config)
|
||
|
||
stock1 = IndividualStockRisk(
|
||
code="600000",
|
||
name="浦发银行",
|
||
volatility=0.3,
|
||
fundamental=0.2,
|
||
liquidity=0.1,
|
||
valuation=0.4,
|
||
recent_vol_change=1.5,
|
||
recent_pct_change=-8.5,
|
||
gap_down=True,
|
||
finance_balance_change=-0.25,
|
||
has_inst_sell=False,
|
||
sector_name="银行",
|
||
sector_style="金融",
|
||
sector_total_gain=15.0,
|
||
sector_position_ratio=0.18
|
||
)
|
||
|
||
result1 = rc.assess_stock(stock1)
|
||
print(f"单个股评估 {result1['name']}({result1['code']}):")
|
||
print(f" 总风险: {result1['total_risk']:.2f}")
|
||
print(f" 风险等级: {result1['risk_level']}")
|
||
print(f" 建议: {result1['suggestion']}")
|
||
print(f" 警告: {result1['warnings']}")
|
||
print()
|
||
|
||
# 测试2: 板块择时
|
||
sectors = [
|
||
SectorInfo(
|
||
name="AI算力",
|
||
style="AI",
|
||
recent_gain=65.0,
|
||
position_ratio=0.22,
|
||
stock_count=4,
|
||
is_hot=True
|
||
),
|
||
SectorInfo(
|
||
name="新能源",
|
||
style="新能源",
|
||
recent_gain=-25.0,
|
||
position_ratio=0.10,
|
||
stock_count=2,
|
||
is_hot=False
|
||
)
|
||
]
|
||
print(rc.get_sector_timing_report(sectors))
|
||
print()
|
||
|
||
# 测试3: 组合评估
|
||
stocks = [stock1]
|
||
portfolio_result = rc.assess_portfolio(stocks, sectors)
|
||
print(rc.get_risk_report(portfolio_result))
|