291 lines
10 KiB
Python
291 lines
10 KiB
Python
"""
|
|
风控模块 - 量化策略风控系统
|
|
功能:
|
|
1. 单票15%止损规则
|
|
2. 整体回撤分级风控(10%/20%/25% 分级降仓)
|
|
3. 黑天鹅过滤(ST、跌停、财务造假排除)
|
|
|
|
Author: 关羽(云长)
|
|
Date: 2026-03-27
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import List, Dict, Optional
|
|
import pandas as pd
|
|
|
|
|
|
@dataclass
|
|
class StockInfo:
|
|
"""单票基本信息"""
|
|
code: str
|
|
name: str
|
|
cost_price: float
|
|
current_price: float
|
|
is_st: bool = False
|
|
is_limit_down: bool = False
|
|
is_fraud: bool = False
|
|
volume: float = 0.0 # 日成交额(亿)
|
|
|
|
|
|
@dataclass
|
|
class PortfolioInfo:
|
|
"""组合信息"""
|
|
total_capital: float
|
|
current_capital: float
|
|
positions: Dict[str, float] # code -> position_size
|
|
|
|
|
|
class SingleStockRiskControl:
|
|
"""单票风控:15%止损规则"""
|
|
|
|
def __init__(self, stop_loss_pct: float = 0.15):
|
|
"""
|
|
初始化
|
|
:param stop_loss_pct: 止损比例,默认15%
|
|
"""
|
|
self.stop_loss_pct = stop_loss_pct
|
|
|
|
def check_stop_loss(self, stock: StockInfo) -> bool:
|
|
"""
|
|
检查是否触发止损
|
|
:return: True = 需要止损,False = 持有
|
|
"""
|
|
if stock.cost_price <= 0:
|
|
return False
|
|
|
|
drawdown = (stock.current_price - stock.cost_price) / stock.cost_price
|
|
# 亏损超过止损比例,触发止损
|
|
return drawdown <= -self.stop_loss_pct
|
|
|
|
def get_drawdown(self, stock: StockInfo) -> float:
|
|
"""计算单票当前回撤"""
|
|
if stock.cost_price <= 0:
|
|
return 0.0
|
|
return (stock.current_price - stock.cost_price) / stock.cost_price
|
|
|
|
|
|
class PortfolioDrawdownRiskControl:
|
|
"""
|
|
整体回撤分级风控
|
|
10%回撤 → 降仓50%
|
|
20%回撤 → 降仓75%
|
|
25%回撤 → 清仓休息
|
|
"""
|
|
|
|
def __init__(self,
|
|
drawdown_levels: List[float] = None,
|
|
reduce_ratios: List[float] = None):
|
|
"""
|
|
初始化分级风控
|
|
:param drawdown_levels: 回撤阈值
|
|
:param reduce_ratios: 对应降仓比例(剩余仓位比例)
|
|
"""
|
|
# 默认分级:10%/20%/25%
|
|
self.drawdown_levels = drawdown_levels or [0.10, 0.20, 0.25]
|
|
# 对应剩余仓位:50% / 25% / 0%
|
|
self.reduce_ratios = reduce_ratios or [0.50, 0.25, 0.00]
|
|
|
|
def calculate_total_drawdown(self, portfolio: PortfolioInfo) -> float:
|
|
"""计算组合总回撤"""
|
|
if portfolio.total_capital <= 0:
|
|
return 0.0
|
|
return (portfolio.total_capital - portfolio.current_capital) / portfolio.total_capital
|
|
|
|
def get_target_position_ratio(self, portfolio: PortfolioInfo) -> float:
|
|
"""
|
|
获取目标仓位比例
|
|
:return: 目标仓位占当前总资金的比例
|
|
"""
|
|
drawdown = self.calculate_total_drawdown(portfolio)
|
|
|
|
# 从大到小检查,触发最高级别
|
|
for level, ratio in reversed(list(zip(self.drawdown_levels, self.reduce_ratios))):
|
|
if drawdown >= level:
|
|
return ratio
|
|
|
|
# 没有触发任何分级,保持满仓
|
|
return 1.0
|
|
|
|
def need_rebalance(self, portfolio: PortfolioInfo) -> tuple[bool, float]:
|
|
"""
|
|
检查是否需要降仓
|
|
:return: (是否需要调整, 目标仓位比例)
|
|
"""
|
|
target_ratio = self.get_target_position_ratio(portfolio)
|
|
current_ratio = sum(portfolio.positions.values()) / portfolio.current_capital
|
|
|
|
# 当前仓位高于目标,需要降仓
|
|
if current_ratio > target_ratio + 0.05: # 5%容差
|
|
return True, target_ratio
|
|
|
|
return False, target_ratio
|
|
|
|
|
|
class BlackSwanFilter:
|
|
"""黑天鹅过滤:排除ST、跌停、财务造假、低流动性票"""
|
|
|
|
def __init__(self, min_daily_volume: float = 0.5):
|
|
"""
|
|
初始化过滤器
|
|
:param min_daily_volume: 最小日成交额(亿),低于此排除
|
|
"""
|
|
self.min_daily_volume = min_daily_volume
|
|
|
|
def filter_stock(self, stock: StockInfo) -> tuple[bool, str]:
|
|
"""
|
|
过滤个股
|
|
:return: (是否通过过滤, 不通过原因)
|
|
True = 可以买入,False = 排除
|
|
"""
|
|
# 1. 排除ST股
|
|
if stock.is_st:
|
|
return False, "ST股票"
|
|
|
|
# 2. 排除跌停
|
|
if stock.is_limit_down:
|
|
return False, "跌停股票"
|
|
|
|
# 3. 排除财务造假
|
|
if stock.is_fraud:
|
|
return False, "财务造假问题股"
|
|
|
|
# 4. 排除低流动性
|
|
if stock.volume < self.min_daily_volume and stock.volume > 0:
|
|
return False, f"成交额不足{self.min_daily_volume}亿,流动性不足"
|
|
|
|
# 全部通过
|
|
return True, ""
|
|
|
|
def filter_universe(self, stocks: List[StockInfo]) -> List[StockInfo]:
|
|
"""批量过滤选股池"""
|
|
passed = []
|
|
for stock in stocks:
|
|
ok, _ = self.filter_stock(stock)
|
|
if ok:
|
|
passed.append(stock)
|
|
return passed
|
|
|
|
|
|
class RiskController:
|
|
"""总风控控制器,整合所有风控规则"""
|
|
|
|
def __init__(self):
|
|
self.single_stock_rc = SingleStockRiskControl()
|
|
self.portfolio_rc = PortfolioDrawdownRiskControl()
|
|
self.black_swan_filter = BlackSwanFilter()
|
|
|
|
def pre_trade_check(self, stock: StockInfo, portfolio: PortfolioInfo) -> tuple[bool, str]:
|
|
"""
|
|
交易前检查:开仓前调用
|
|
:return: (是否允许开仓, 原因)
|
|
"""
|
|
# 第一步:黑天鹅过滤
|
|
ok, reason = self.black_swan_filter.filter_stock(stock)
|
|
if not ok:
|
|
return False, f"黑天鹅过滤: {reason}"
|
|
|
|
# 第二步:检查整体回撤是否允许新开仓
|
|
target_ratio = self.portfolio_rc.get_target_position_ratio(portfolio)
|
|
current_ratio = sum(portfolio.positions.values()) / portfolio.current_capital
|
|
|
|
if current_ratio >= target_ratio:
|
|
return False, f"整体回撤已触发降仓,当前仓位{current_ratio:.1%},目标仓位{target_ratio:.1%},不允许新开仓"
|
|
|
|
return True, ""
|
|
|
|
def post_trade_check(self, stocks: List[StockInfo], portfolio: PortfolioInfo) -> dict:
|
|
"""
|
|
收盘后检查:止损检查 + 降仓检查
|
|
:return: 风控结果,包含需要止损的票和需要降仓的信息
|
|
"""
|
|
# 1. 检查单票止损
|
|
stop_loss_list = []
|
|
for stock in stocks:
|
|
if self.single_stock_rc.check_stop_loss(stock):
|
|
stop_loss_list.append({
|
|
"code": stock.code,
|
|
"name": stock.name,
|
|
"current_drawdown": self.single_stock_rc.get_drawdown(stock)
|
|
})
|
|
|
|
# 2. 检查组合降仓
|
|
need_rebalance, target_ratio = self.portfolio_rc.need_rebalance(portfolio)
|
|
current_drawdown = self.portfolio_rc.calculate_total_drawdown(portfolio)
|
|
|
|
return {
|
|
"stop_loss_required": len(stop_loss_list) > 0,
|
|
"stop_loss_stocks": stop_loss_list,
|
|
"rebalance_required": need_rebalance,
|
|
"current_drawdown": current_drawdown,
|
|
"target_position_ratio": target_ratio,
|
|
"current_position_ratio": sum(portfolio.positions.values()) / portfolio.current_capital if portfolio.current_capital > 0 else 0
|
|
}
|
|
|
|
def get_risk_report(self, stocks: List[StockInfo], portfolio: PortfolioInfo) -> str:
|
|
"""生成风控报告"""
|
|
result = self.post_trade_check(stocks, portfolio)
|
|
|
|
report = []
|
|
report.append("=" * 50)
|
|
report.append("风控日报")
|
|
report.append("=" * 50)
|
|
report.append(f"组合总回撤: {result['current_drawdown']:.2%}")
|
|
report.append(f"当前仓位比例: {result['current_position_ratio']:.2%}")
|
|
report.append(f"目标仓位比例: {result['target_position_ratio']:.2%}")
|
|
report.append("")
|
|
|
|
if result['stop_loss_required']:
|
|
report.append("触发止损个股:")
|
|
for s in result['stop_loss_stocks']:
|
|
report.append(f" {s['code']} {s['name']} 回撤: {s['current_drawdown']:.2%}")
|
|
else:
|
|
report.append("无个股触发止损")
|
|
|
|
report.append("")
|
|
if result['rebalance_required']:
|
|
report.append(f"⚠️ 需要降仓调整,当前仓位高于目标")
|
|
else:
|
|
report.append("✅ 仓位符合风控要求")
|
|
|
|
report.append("=" * 50)
|
|
return "\n".join(report)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 简单测试
|
|
from pprint import pprint
|
|
|
|
# 测试单票止损
|
|
stock1 = StockInfo(code="000001", name="平安银行", cost_price=10.0, current_price=8.4)
|
|
rc_single = SingleStockRiskControl()
|
|
print(f"测试单票止损,当前价8.4,成本10,亏损16%: {rc_single.check_stop_loss(stock1)} → 应该是True")
|
|
|
|
stock2 = StockInfo(code="000002", name="万科A", cost_price=20.0, current_price=18.0)
|
|
print(f"测试单票止损,当前价18,成本20,亏损10%: {rc_single.check_stop_loss(stock2)} → 应该是False")
|
|
|
|
# 测试组合回撤
|
|
portfolio = PortfolioInfo(
|
|
total_capital=1000000,
|
|
current_capital=850000,
|
|
positions={"000001": 200000, "000002": 150000}
|
|
)
|
|
rc_port = PortfolioDrawdownRiskControl()
|
|
dd = rc_port.calculate_total_drawdown(portfolio)
|
|
print(f"\n组合回撤: {dd:.2%} → 15%")
|
|
print(f"目标仓位比例: {rc_port.get_target_position_ratio(portfolio)} → 50%(触发10%档)")
|
|
|
|
# 测试黑天鹅过滤
|
|
filter_bs = BlackSwanFilter()
|
|
st_stock = StockInfo(code="000003", name="ST基蛋", cost_price=10, current_price=10, is_st=True)
|
|
ok, reason = filter_bs.filter_stock(st_stock)
|
|
print(f"\nST过滤: ok={ok}, reason={reason}")
|
|
|
|
normal_stock = StockInfo(code="600000", name="浦发银行", cost_price=10, current_price=10, volume=1.2)
|
|
ok, reason = filter_bs.filter_stock(normal_stock)
|
|
print(f"正常票过滤: ok={ok}, reason={reason}")
|
|
|
|
# 整合风控测试
|
|
rc = RiskController()
|
|
print("\n风控报告:")
|
|
print(rc.get_risk_report([stock1, stock2], portfolio))
|