Files
sanguo_quant_live/strategies/structured-dynamic-factors-20260327/risk_control.py
T

542 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
结构化适配动态多因子 - 风控和择时模块
功能:
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))