Files
sanguo_quant_live/guanyu-risk/common/news_risk_monitor.py
T

630 lines
25 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. 舆情频率:股吧雪球讨论热度异常变化
5. 国际联动风险:外盘、商品、国际消息对A股影响预判
6. 结构化行情风控:板块集中度风险、热点透支风险
Author: 关羽(云长)
Date: 2026-03-27
"""
from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
from enum import Enum
class NewsSentiment(Enum):
STRONG_BULL = 2
BULL = 1
NEUTRAL = 0
BEAR = -1
STRONG_BEAR = -2
class RiskLevel(Enum):
SAFE = 0
WATCH = 1
REDUCE = 2
EXIT = 3
@dataclass
class StockNewsData:
"""个股监控数据"""
code: str
name: str
# 1. 量价数据
recent_vol_change: float # 近5日成交量相比20日均值变化比例,比如0.5就是放量50%
recent_pct_change: float # 近5日涨跌幅(%
gap_up: bool = False # 是否高开缺口未回补
gap_down: bool = False # 是否低开缺口未回补
# 2. 融资数据
finance_balance_change: float = 0.0 # 融资余额变化比例(周)
finance_pct_of_volume: float = 0.0 # 融资买入额占成交额比例
# 3. 龙虎榜/大宗
has_large_order: bool = False # 近日是否有龙虎榜大额卖出
has_bulk_discount: bool = False # 是否有大宗折价交易
bulk_discount_pct: float = 0.0 # 大宗折价幅度
# 4. 舆情数据
discussion_count_change: float = 0.0 # 讨论量相比上周变化倍数
sentiment_score: float = 0.0 # 舆情情感分 -1~1,负=利空
# 5. 国际联动数据
is_ah: bool = False # 是否A+H股
ah_hk_overnight_change: float = 0.0 # 港股H股隔夜涨跌幅%
is_commodity_related: bool = False # 是否大宗商品相关(有色/化工/农业)
commodity_future_overnight_change: float = 0.0 # 对应期货隔夜涨跌幅%
us_index_overnight_change: float = 0.0 # 美股隔夜涨跌%
has_major_international_news: bool = False # 是否有重大国际消息(加息/地缘政治等)
international_news_sentiment: int = 0 # -1利空 0中性 1利好
# 6. 结构化行情数据
sector_name: str = "" # 板块名称
sector_style: str = "" # 风格名称(AI/新能源/消费等)
sector_total_gain: float = 0.0 # 板块近期累计涨幅%
sector_position_ratio: float = 0.0 # 当前板块仓位占总仓位比例
@dataclass
class NewsRiskResult:
"""消息风险评估结果"""
code: str
name: str
risk_level: RiskLevel
sentiment: NewsSentiment
risk_score: float # 总分 0~100,越高风险越大
bear_points: List[str] # 利空信号点
bull_points: List[str] # 利好信号点
suggestion: str # 操作建议
class PriceVolumeMonitor:
"""量价异常监控"""
def __init__(self,
vol_alert_threshold: float = 2.0, # 放量超过2倍预警
vol_crush_threshold: float = -0.5, # 缩量超过50%预警
drop_alert_threshold: float = -7.0): # 5日跌幅超过7%预警
self.vol_alert_threshold = vol_alert_threshold
self.vol_crush_threshold = vol_crush_threshold
self.drop_alert_threshold = drop_alert_threshold
def analyze(self, data: StockNewsData) -> Tuple[int, List[str], List[str]]:
"""
返回:(风险分增量, 利空点列表, 利好点列表)
"""
risk_score = 0
bear_points = []
bull_points = []
# 天量天价无消息,警惕利好兑现出货
if data.recent_vol_change >= self.vol_alert_threshold and data.recent_pct_change >= 5:
risk_score += 15
bear_points.append(f"近5日放量{data.recent_vol_change:.1f}倍,涨幅{data.recent_pct_change:.1f}%,可能利好提前泄露,警惕出货")
# 莫名其妙大跌,可能有利空提前泄露
if data.recent_pct_change <= self.drop_alert_threshold and data.recent_vol_change >= 0.5:
risk_score += 20
bear_points.append(f"近5日放量下跌{data.recent_pct_change:.1f}%,无公开消息,警惕利空提前泄露")
# 向下跳空缺口
if data.gap_down:
risk_score += 10
bear_points.append("存在向下跳空缺口未回补,技术形态偏空")
# 向上跳空缺口
if data.gap_up:
bull_points.append("存在向上跳空缺口未回补,技术形态偏多")
# 突然严重缩量,警惕流动性枯竭
if data.recent_vol_change <= self.vol_crush_threshold:
risk_score += 10
bear_points.append(f"成交量缩量{(data.recent_vol_change*100):.0f}%,警惕流动性风险")
return risk_score, bear_points, bull_points
class FinanceMonitor:
"""融资余额监控"""
def __init__(self,
increase_threshold: float = 0.3, # 融资余额增加超30%
decrease_threshold: float = -0.2): # 融资余额减少超20%
self.increase_threshold = increase_threshold
self.decrease_threshold = decrease_threshold
def analyze(self, data: StockNewsData) -> Tuple[int, List[str], List[str]]:
risk_score = 0
bear_points = []
bull_points = []
# 融资快速增加,看多情绪升温
if data.finance_balance_change >= self.increase_threshold:
bull_points.append(f"融资余额一周增加{data.finance_balance_change:.1%},杠杆资金看多")
# 融资快速减少,资金出逃,利空
if data.finance_balance_change <= self.decrease_threshold:
risk_score += 15
bear_points.append(f"融资余额一周减少{data.finance_balance_change:.1%},杠杆资金快速出逃,警惕利空")
# 融资买入占比过高,波动会放大
if data.finance_pct_of_volume >= 0.2:
risk_score += 5
bear_points.append(f"融资买入占成交额{data.finance_pct_of_volume:.1%},杠杆比例高,波动风险大")
return risk_score, bear_points, bull_points
class InstitutionalMonitor:
"""龙虎榜/大宗交易监控"""
def __init__(self,
bulk_discount_threshold: float = -0.08): # 折价超过8%预警
self.bulk_discount_threshold = bulk_discount_threshold
def analyze(self, data: StockNewsData) -> Tuple[int, List[str], List[str]]:
risk_score = 0
bear_points = []
bull_points = []
# 龙虎榜大额机构卖出
if data.has_large_order:
risk_score += 20
bear_points.append("龙虎榜出现机构大额卖出,机构出逃")
# 大宗折价交易
if data.has_bulk_discount and data.bulk_discount_pct <= self.bulk_discount_threshold:
risk_score += 15
bear_points.append(f"大宗交易折价{data.bulk_discount_pct:.1%},大股东折价出货")
elif data.has_bulk_discount:
bull_points.append("大宗交易平价/溢价成交,有机构接盘")
return risk_score, bear_points, bull_points
class SentimentMonitor:
"""舆情热度监控"""
def __init__(self,
hot_threshold: float = 5.0, # 讨论量增加5倍,太热预警
cold_threshold: float = -0.8): # 讨论量减少80%,太凉预警
self.hot_threshold = hot_threshold
self.cold_threshold = cold_threshold
def analyze(self, data: StockNewsData) -> Tuple[int, List[str], List[str]]:
risk_score = 0
bear_points = []
bull_points = []
# 讨论量突然暴涨,关注度太高,往往是见顶信号
if data.discussion_count_change >= self.hot_threshold:
risk_score += 10
bear_points.append(f"股吧/雪球讨论量增加{data.discussion_count_change:.1f}倍,热度异常,可能见顶")
# 舆情已经明显偏空
if data.sentiment_score <= -0.5:
risk_score += 10
bear_points.append(f"市场舆情偏空,情感分{data.sentiment_score:.2f}")
# 舆情明显偏多
elif data.sentiment_score >= 0.5:
bull_points.append(f"市场舆情偏多,情感分{data.sentiment_score:.2f}")
return risk_score, bear_points, bull_points
class InternationalLinkageMonitor:
"""国际联动风险监控"""
def __init__(self,
ah_change_threshold: float = -3.0, # H股隔夜跌幅超过3%预警
commodity_change_threshold: float = -4.0, # 商品期货跌幅超过4%预警
us_index_change_threshold: float = -2.0): # 美股跌幅超过2%预警
self.ah_change_threshold = ah_change_threshold
self.commodity_change_threshold = commodity_change_threshold
self.us_index_change_threshold = us_index_change_threshold
def analyze(self, data: StockNewsData) -> Tuple[int, List[str], List[str]]:
risk_score = 0
bear_points = []
bull_points = []
# 1. A+H股,H股隔夜大跌预警
if data.is_ah:
if data.ah_hk_overnight_change <= self.ah_change_threshold:
risk_score += 15
bear_points.append(f"A+H股,H股隔夜大跌{data.ah_hk_overnight_change:.1f}%A股大概率跟随低开,风险预警")
elif data.ah_hk_overnight_change >= 3.0:
bull_points.append(f"A+H股,H股隔夜大涨{data.ah_hk_overnight_change:.1f}%,对A股有正面带动")
# 2. 大宗商品相关个股,对应期货隔夜大跌预警
if data.is_commodity_related:
if data.commodity_future_overnight_change <= self.commodity_change_threshold:
risk_score += 15
bear_points.append(f"大宗商品股,对应期货隔夜大跌{data.commodity_future_overnight_change:.1f}%,个股承压")
elif data.commodity_future_overnight_change >= 4.0:
bull_points.append(f"大宗商品股,对应期货隔夜大涨{data.commodity_future_overnight_change:.1f}%,对个股有利")
# 3. 美股隔夜大跌,系统性风险预警
if data.us_index_overnight_change <= self.us_index_change_threshold:
risk_score += 10
bear_points.append(f"美股隔夜大跌{data.us_index_overnight_change:.1f}%A股开盘可能承压,系统性风险")
elif data.us_index_overnight_change >= 2.0:
bull_points.append(f"美股隔夜大涨{data.us_index_overnight_change:.1f}%,对A股开盘有利")
# 4. 重大国际消息
if data.has_major_international_news:
if data.international_news_sentiment == -1:
risk_score += 20
bear_points.append("重大国际利空消息(加息/地缘政治等),系统性风险上升,建议降仓")
elif data.international_news_sentiment == 1:
bull_points.append("重大国际利好消息,市场情绪偏向乐观")
return risk_score, bear_points, bull_points
class StructuralMarketRisk:
"""
结构化行情风控
A股现在经常是结构化行情,少数板块上涨,其他板块不动
需要控制板块集中度,防范热点透支
"""
def __init__(self,
single_sector_threshold: float = 0.15, # 单板块仓位超15%预警
sector_rally_threshold: float = 50.0, # 板块累计涨幅超50%预警
hot_news_sensitivity: float = 2.0): # 热点板块消息风险放大倍数
self.single_sector_threshold = single_sector_threshold
self.sector_rally_threshold = sector_rally_threshold
self.hot_news_sensitivity = hot_news_sensitivity
def analyze_sector_risk(self, data: StockNewsData, total_portfolio_sectors: Dict[str, float]) -> Tuple[int, List[str], List[str]]:
"""
分析板块风险
total_portfolio_sectors: {板块名称: 板块仓位比例},用来计算整体板块集中度
"""
risk_score = 0
bear_points = []
bull_points = []
# 1. 单板块仓位超过阈值,提醒分散
if data.sector_position_ratio > self.single_sector_threshold:
extra_risk = int((data.sector_position_ratio - self.single_sector_threshold) * 100)
risk_score += extra_risk
bear_points.append(f"单板块仓位{data.sector_position_ratio:.1%},超过{self.single_sector_threshold:.1%}预警,建议分散减仓")
# 2. 板块连续大涨,提醒风险
if data.sector_total_gain >= self.sector_rally_threshold:
risk_score += 15
bear_points.append(f"板块{data.sector_name}累计涨幅{data.sector_total_gain:.1f}%,超过{self.sector_rally_threshold:.0f}%,警惕利好出尽")
# 3. 冷门板块连续大跌,基本面没问题提示低吸机会
# 这里只做提示,实际需要结合基本面判断
if data.sector_total_gain <= -20 and data.sector_total_gain >= -40:
bull_points.append(f"板块{data.sector_name}累计跌幅{data.sector_total_gain:.1f}%,如果基本面没问题,可考虑适度低吸")
return risk_score, bear_points, bull_points
def adjust_news_risk_for_hot_sector(self, base_risk: int, data: StockNewsData) -> int:
"""热点板块消息风险灵敏度提高,放大风险分"""
if data.sector_total_gain >= 30:
# 热点板块,消息更容易透支,放大风险评分
return int(base_risk * self.hot_news_sensitivity)
return base_risk
class StyleConcentrationRisk:
"""风格集中度风险控制,不要全仓押注一种风格"""
def __init__(self, single_style_threshold: float = 0.4): # 单一风格超40%预警
self.single_style_threshold = single_style_threshold
def analyze_style_risk(self, total_style_pos: Dict[str, float]) -> Tuple[int, List[str]]:
"""检查风格集中度"""
risk_score = 0
bear_points = []
for style, ratio in total_style_pos.items():
if ratio >= self.single_style_threshold:
extra_risk = int((ratio - self.single_style_threshold) * 50)
risk_score += extra_risk
bear_points.append(f"单一风格{style}仓位{ratio:.1%},超过{self.single_style_threshold:.0%},建议分散配置")
return risk_score, bear_points
class NewsRiskMonitor:
"""总消息风险监控器,整合所有监控维度,适配结构化行情"""
def __init__(self):
self.pv_monitor = PriceVolumeMonitor()
self.fin_monitor = FinanceMonitor()
self.inst_monitor = InstitutionalMonitor()
self.sent_monitor = SentimentMonitor()
self.intl_monitor = InternationalLinkageMonitor()
self.structural_rc = StructuralMarketRisk()
def analyze_stock(self, data: StockNewsData, total_portfolio_sectors: Dict[str, float] = None) -> NewsRiskResult:
"""综合分析个股消息风险,支持结构化板块风控"""
total_portfolio_sectors = total_portfolio_sectors or {}
total_risk = 0
all_bear = []
all_bull = []
# 原有各维度打分
r1, b1, bl1 = self.pv_monitor.analyze(data)
r2, b2, bl2 = self.fin_monitor.analyze(data)
r3, b3, bl3 = self.inst_monitor.analyze(data)
r4, b4, bl4 = self.sent_monitor.analyze(data)
r5, b5, bl5 = self.intl_monitor.analyze(data)
total_risk = r1 + r2 + r3 + r4 + r5
all_bear = b1 + b2 + b3 + b4 + b5
all_bull = bl1 + bl2 + bl3 + bl4 + bl5
# 新增结构化行情板块风控
if data.sector_name:
r6, b6, bl6 = self.structural_rc.analyze_sector_risk(data, total_portfolio_sectors)
# 热点板块消息风险放大
base_news_risk = total_risk
total_risk = self.structural_rc.adjust_news_risk_for_hot_sector(base_news_risk, data) + r6
all_bear.extend(b6)
all_bull.extend(bl6)
# 计算风险等级
# 计算风险等级
if total_risk >= 40:
risk_level = RiskLevel.EXIT
elif total_risk >= 25:
risk_level = RiskLevel.REDUCE
elif total_risk >= 10:
risk_level = RiskLevel.WATCH
else:
risk_level = RiskLevel.SAFE
# 计算整体情绪
bull_count = len(all_bull)
bear_count = len(all_bear)
if bull_count - bear_count >= 2:
sentiment = NewsSentiment.STRONG_BULL
elif bull_count - bear_count >= 1:
sentiment = NewsSentiment.BULL
elif bear_count - bull_count >= 2:
sentiment = NewsSentiment.STRONG_BEAR
elif bear_count - bull_count >= 1:
sentiment = NewsSentiment.BEAR
else:
sentiment = NewsSentiment.NEUTRAL
# 生成操作建议
suggestion = self._get_suggestion(risk_level, sentiment)
return NewsRiskResult(
code=data.code,
name=data.name,
risk_level=risk_level,
sentiment=sentiment,
risk_score=total_risk,
bear_points=all_bear,
bull_points=all_bull,
suggestion=suggestion
)
def analyze_stocks(self, datas: List[StockNewsData]) -> List[NewsRiskResult]:
"""批量分析"""
return [self.analyze_stock(d) for d in datas]
def _get_suggestion(self, risk_level: RiskLevel, sentiment: NewsSentiment) -> str:
"""根据风险和情绪给出操作建议"""
if risk_level == RiskLevel.EXIT:
return "⚠️ 风险较高,建议减仓或清仓离场,规避黑天鹅"
elif risk_level == RiskLevel.REDUCE:
return "⚠️ 存在明显利空信号,建议降低仓位,控制风险"
elif risk_level == RiskLevel.WATCH:
if sentiment in [NewsSentiment.BULL, NewsSentiment.STRONG_BULL]:
return "👀 有少量异常信号,但整体偏多,可继续观察,谨慎加仓"
else:
return "👀 存在轻度风险信号,继续观察,不新开仓"
else:
if sentiment in [NewsSentiment.BULL, NewsSentiment.STRONG_BULL]:
return "✅ 无明显风险,整体偏多,可按计划操作"
else:
return "✅ 无明显风险,按计划持有"
def get_risk_report(self, result: NewsRiskResult) -> str:
"""生成风险报告"""
levels = {
RiskLevel.SAFE: "🟢 安全",
RiskLevel.WATCH: "🟡 关注",
RiskLevel.REDUCE: "🟠 减仓",
RiskLevel.EXIT: "🔴 离场",
}
sentiments = {
NewsSentiment.STRONG_BULL: "🔼 强烈看多",
NewsSentiment.BULL: "▶️ 看多",
NewsSentiment.NEUTRAL: " 中性",
NewsSentiment.BEAR: "◀️ 看空",
NewsSentiment.STRONG_BEAR: "🔽 强烈看空",
}
lines = []
lines.append("=" * 60)
lines.append(f"个股消息风险评估: {result.name}({result.code})")
lines.append("=" * 60)
lines.append(f"风险等级: {levels[result.risk_level]} 风险分: {result.risk_score}/100")
lines.append(f"市场情绪: {sentiments[result.sentiment]}")
lines.append("")
if result.bull_points:
lines.append("✅ 利好信号:")
for point in result.bull_points:
lines.append(f"{point}")
lines.append("")
if result.bear_points:
lines.append("⚠️ 利空信号:")
for point in result.bear_points:
lines.append(f"{point}")
lines.append("")
lines.append(f"💡 操作建议: {result.suggestion}")
lines.append("=" * 60)
return "\n".join(lines)
def get_risk_score_for_risk_system(self, result: NewsRiskResult) -> float:
"""
获取归一化风险分,给五维风险评估体系使用
返回 0~1,越高风险越大
"""
return min(result.risk_score / 50, 1.0)
# 整合到原有风控体系的五维风险评估
class FiveDimensionRiskAssessment:
"""
五维度风险评估
整合:流动性风险 + 估值风险 + 技术面风险 + 基本面风险 + 消息面风险
"""
def __init__(self, news_monitor: NewsRiskMonitor = None):
self.news_monitor = news_monitor or NewsRiskMonitor()
def calculate_total_risk(self,
liquidity_risk: float, # 0~1
valuation_risk: float, # 0~1
technical_risk: float, # 0~1
fundamental_risk: float, # 0~1
news_data: StockNewsData) -> dict:
"""
计算五维综合风险
每个维度输入都是0~1,越高风险越大
"""
# 先算消息面风险
news_result = self.news_monitor.analyze_stock(news_data)
news_risk = self.news_monitor.get_risk_score_for_risk_system(news_result)
# 加权平均,消息面风险权重高一些,因为黑天鹅突发危害大
total_risk = (
liquidity_risk * 0.15 +
valuation_risk * 0.20 +
technical_risk * 0.20 +
fundamental_risk * 0.20 +
news_risk * 0.25
)
return {
"total_risk_score": total_risk, # 0~1
"dimension_scores": {
"liquidity": liquidity_risk,
"valuation": valuation_risk,
"technical": technical_risk,
"fundamental": fundamental_risk,
"news": news_risk
},
"news_result": news_result,
"overall_risk_level": self._get_level(total_risk),
"suggestion": self._get_suggestion(total_risk)
}
def _get_level(self, score: float) -> str:
if score >= 0.7:
return "高风险"
elif score >= 0.4:
return "中风险"
else:
return "低风险"
def _get_suggestion(self, score: float) -> str:
if score >= 0.7:
return "不参与,规避"
elif score >= 0.4:
return "轻仓参与,严格止损"
else:
return "正常参与,按计划执行"
if __name__ == "__main__":
# 测试案例
print("=== 测试1: 疑似提前泄露利空的个股 + 国际利空 ===")
data1 = StockNewsData(
code="600XXX",
name="XX股份",
recent_vol_change=1.2, # 放量120%
recent_pct_change=-8.5, # 5天下跌8.5%
gap_down=True,
finance_balance_change=-0.25, # 融资减少25%
discussion_count_change=3.0,
sentiment_score=-0.6,
has_major_international_news=True,
international_news_sentiment=-1
)
monitor = NewsRiskMonitor()
result1 = monitor.analyze_stock(data1)
print(monitor.get_risk_report(result1))
print("\n=== 测试2: 明显利好信号个股 ===")
data2 = StockNewsData(
code="002XXX",
name="XX科技",
recent_vol_change=0.8,
recent_pct_change=4.2,
gap_up=True,
finance_balance_change=0.4, # 融资增加40%
has_large_order=False,
discussion_count_change=2.0,
sentiment_score=0.5
)
result2 = monitor.analyze_stock(data2)
print(monitor.get_risk_report(result2))
print("\n=== 测试2: 大宗商品股 + 商品隔夜大跌 ===")
data2 = StockNewsData(
code="601899",
name="紫金矿业",
recent_vol_change=0.5,
recent_pct_change=-2.0,
is_commodity_related=True,
commodity_future_overnight_change=-5.2,
us_index_overnight_change=-2.5
)
result2 = monitor.analyze_stock(data2)
print(monitor.get_risk_report(result2))
print("\n=== 测试3: A+H股 + H股隔夜大跌 ===")
data3 = StockNewsData(
code="600016",
name="民生银行",
recent_vol_change=0.1,
recent_pct_change=-1.2,
is_ah=True,
ah_hk_overnight_change=-4.5
)
result3 = monitor.analyze_stock(data3)
print(monitor.get_risk_report(result3))
print("\n=== 测试4: 五维风险评估 ===")
five_dim = FiveDimensionRiskAssessment()
eval_result = five_dim.calculate_total_risk(
liquidity_risk=0.2,
valuation_risk=0.5,
technical_risk=0.3,
fundamental_risk=0.4,
news_data=data1
)
print(f"五维综合风险评分: {eval_result['total_risk_score']:.2f}")
print(f"风险等级: {eval_result['overall_risk_level']}")
print(f"建议: {eval_result['suggestion']}")
print(f"各维度分数: {eval_result['dimension_scores']}")