377 lines
14 KiB
Python
377 lines
14 KiB
Python
"""
|
||
纯突破策略风控模块
|
||
功能:
|
||
1. 单票止损:跌破突破日最低价5% → 止损卖出
|
||
2. 单日暴跌超过7% → 强制减仓
|
||
3. 全仓回撤控制:整体回撤超过10%降仓,超过20%清半仓
|
||
4. 排除过滤:ST、涨跌停不能买、日均成交额<5000万排除
|
||
|
||
Author: 关羽(云长)
|
||
Date: 2026-03-27
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
from typing import List, Dict, Optional, Tuple
|
||
import pandas as pd
|
||
|
||
|
||
@dataclass
|
||
class BreakoutStock:
|
||
"""突破策略单票信息"""
|
||
code: str
|
||
name: str
|
||
breakout_date: str # 突破日期
|
||
breakout_low: float # 突破日最低价
|
||
current_price: float
|
||
daily_change: float # 当日涨跌幅(%)
|
||
is_st: bool = False
|
||
is_limit_up: bool = False
|
||
is_limit_down: bool = False
|
||
avg_daily_volume: float = 0.0 # 日均成交额(亿)
|
||
position_size: float = 0.0 # 当前持仓市值
|
||
|
||
|
||
@dataclass
|
||
class PortfolioInfo:
|
||
"""组合信息"""
|
||
initial_capital: float
|
||
current_capital: float
|
||
positions: Dict[str, float] # code -> position_size
|
||
|
||
|
||
class BreakoutSingleStopLoss:
|
||
"""单票止损规则:跌破突破日最低价5%止损"""
|
||
|
||
def __init__(self, trigger_pct: float = 0.05):
|
||
"""
|
||
初始化
|
||
:param trigger_pct: 跌破突破日最低价多少比例触发止损,默认5%
|
||
"""
|
||
self.trigger_pct = trigger_pct
|
||
|
||
def check_stop_loss(self, stock: BreakoutStock) -> Tuple[bool, float]:
|
||
"""
|
||
检查是否触发止损
|
||
:return: (是否触发, 当前价格相对突破低点跌幅)
|
||
"""
|
||
if stock.breakout_low <= 0:
|
||
return False, 0.0
|
||
|
||
drop_pct = (stock.breakout_low - stock.current_price) / stock.breakout_low
|
||
# 当前价格跌破突破日最低价超过5%,触发止损
|
||
return drop_pct >= self.trigger_pct, drop_pct
|
||
|
||
|
||
class SingleDayCrashReduce:
|
||
"""单日暴跌强制减仓:单日暴跌超过7%强制减仓"""
|
||
|
||
def __init__(self, crash_threshold: float = -7.0, reduce_ratio: float = 0.5):
|
||
"""
|
||
初始化
|
||
:param crash_threshold: 暴跌阈值(%),默认-7%
|
||
:param reduce_ratio: 减仓比例,卖出一半,默认0.5
|
||
"""
|
||
self.crash_threshold = crash_threshold
|
||
self.reduce_ratio = reduce_ratio
|
||
|
||
def check_crash(self, stock: BreakoutStock) -> Tuple[bool, float]:
|
||
"""检查是否触发暴跌减仓"""
|
||
# 单日跌幅超过阈值,触发减仓
|
||
return stock.daily_change <= self.crash_threshold, stock.daily_change
|
||
|
||
def get_target_position(self, current_position: float) -> float:
|
||
"""计算目标持仓"""
|
||
return current_position * (1 - self.reduce_ratio)
|
||
|
||
|
||
class PortfolioDrawdownControl:
|
||
"""
|
||
全仓回撤控制
|
||
整体回撤 > 10% → 降仓到50%
|
||
整体回撤 > 20% → 清半仓,保留25%
|
||
"""
|
||
|
||
def __init__(self,
|
||
drawdown_1: float = 0.10,
|
||
drawdown_2: float = 0.20,
|
||
target_ratio_1: float = 0.50,
|
||
target_ratio_2: float = 0.25):
|
||
"""
|
||
初始化
|
||
:param drawdown_1: 一级回撤阈值
|
||
:param drawdown_2: 二级回撤阈值
|
||
:param target_ratio_1: 一级触发后目标仓位比例
|
||
:param target_ratio_2: 二级触发后目标仓位比例
|
||
"""
|
||
self.drawdown_1 = drawdown_1
|
||
self.drawdown_2 = drawdown_2
|
||
self.target_ratio_1 = target_ratio_1
|
||
self.target_ratio_2 = target_ratio_2
|
||
|
||
def calculate_drawdown(self, portfolio: PortfolioInfo) -> float:
|
||
"""计算组合总回撤"""
|
||
if portfolio.initial_capital <= 0:
|
||
return 0.0
|
||
return (portfolio.initial_capital - portfolio.current_capital) / portfolio.initial_capital
|
||
|
||
def get_target_position_ratio(self, portfolio: PortfolioInfo) -> float:
|
||
"""获取目标仓位比例"""
|
||
drawdown = self.calculate_drawdown(portfolio)
|
||
|
||
if drawdown >= self.drawdown_2:
|
||
return self.target_ratio_2
|
||
elif drawdown >= self.drawdown_1:
|
||
return self.target_ratio_1
|
||
else:
|
||
return 1.0 # 回撤不超,保持满仓
|
||
|
||
def need_adjust(self, portfolio: PortfolioInfo) -> Tuple[bool, float]:
|
||
"""检查是否需要调整仓位"""
|
||
target_ratio = self.get_target_position_ratio(portfolio)
|
||
current_total = sum(portfolio.positions.values())
|
||
current_ratio = current_total / portfolio.current_capital
|
||
|
||
if current_ratio > target_ratio + 0.05: # 5%容差
|
||
return True, target_ratio
|
||
|
||
return False, target_ratio
|
||
|
||
|
||
class BreakoutPreTradeFilter:
|
||
"""突破策略开仓前过滤:排除风险票"""
|
||
|
||
def __init__(self, min_avg_volume: float = 0.5):
|
||
"""
|
||
初始化
|
||
:param min_avg_volume: 最小日均成交额(亿),默认5000万
|
||
"""
|
||
self.min_avg_volume = min_avg_volume
|
||
|
||
def filter_stock(self, stock: BreakoutStock) -> Tuple[bool, str]:
|
||
"""
|
||
开仓前过滤
|
||
:return: (是否可以开仓, 不通过原因)
|
||
"""
|
||
# 1. 排除ST
|
||
if stock.is_st:
|
||
return False, "ST股票,排除"
|
||
|
||
# 2. 涨跌停都不能买
|
||
if stock.is_limit_up:
|
||
return False, "涨停买不进,排除"
|
||
if stock.is_limit_down:
|
||
return False, "跌停不能买,排除"
|
||
|
||
# 3. 流动性不足排除
|
||
if stock.avg_daily_volume < self.min_avg_volume and stock.avg_daily_volume > 0:
|
||
return False, f"日均成交额{stock.avg_daily_volume:.1f}亿,不足{self.min_avg_volume}亿,排除"
|
||
|
||
# 全部通过
|
||
return True, ""
|
||
|
||
def filter_universe(self, stocks: List[BreakoutStock]) -> List[BreakoutStock]:
|
||
"""批量过滤候选池"""
|
||
result = []
|
||
for stock in stocks:
|
||
ok, _ = self.filter_stock(stock)
|
||
if ok:
|
||
result.append(stock)
|
||
return result
|
||
|
||
|
||
class BreakoutRiskController:
|
||
"""突破策略总风控控制器,整合所有风控规则"""
|
||
|
||
def __init__(self):
|
||
self.stop_loss_rc = BreakoutSingleStopLoss()
|
||
self.crash_rc = SingleDayCrashReduce()
|
||
self.portfolio_rc = PortfolioDrawdownControl()
|
||
self.filter_rc = BreakoutPreTradeFilter()
|
||
|
||
def pre_trade_check(self, stock: BreakoutStock, portfolio: PortfolioInfo) -> Tuple[bool, str]:
|
||
"""
|
||
开仓前检查
|
||
:return: (是否允许开仓, 原因)
|
||
"""
|
||
# 第一步:过滤检查
|
||
ok, reason = self.filter_rc.filter_stock(stock)
|
||
if not ok:
|
||
return False, reason
|
||
|
||
# 第二步:仓位检查
|
||
need_adjust, target_ratio = self.portfolio_rc.need_adjust(portfolio)
|
||
current_total = sum(portfolio.positions.values())
|
||
current_ratio = current_total / portfolio.current_capital
|
||
|
||
if current_ratio >= target_ratio:
|
||
return False, f"整体回撤{self.portfolio_rc.calculate_drawdown(portfolio):.1%},目标仓位{target_ratio:.1%},当前已满仓,不允许开新仓"
|
||
|
||
return True, "通过"
|
||
|
||
def post_trade_check(self, stocks: List[BreakoutStock], portfolio: PortfolioInfo) -> dict:
|
||
"""
|
||
收盘后检查,找出需要止损减仓的标的
|
||
:return: 风控检查结果
|
||
"""
|
||
# 1. 检查突破低点止损
|
||
stop_loss_list = []
|
||
for stock in stocks:
|
||
triggered, drop = self.stop_loss_rc.check_stop_loss(stock)
|
||
if triggered:
|
||
stop_loss_list.append({
|
||
"code": stock.code,
|
||
"name": stock.name,
|
||
"breakout_low": stock.breakout_low,
|
||
"current_price": stock.current_price,
|
||
"drop_pct": drop
|
||
})
|
||
|
||
# 2. 检查单日暴跌减仓
|
||
crash_reduce_list = []
|
||
for stock in stocks:
|
||
triggered, change = self.crash_rc.check_crash(stock)
|
||
if triggered:
|
||
crash_reduce_list.append({
|
||
"code": stock.code,
|
||
"name": stock.name,
|
||
"daily_change": change,
|
||
"current_position": stock.position_size,
|
||
"target_position": self.crash_rc.get_target_position(stock.position_size)
|
||
})
|
||
|
||
# 3. 检查组合整体回撤调整
|
||
need_portfolio_adjust, target_ratio = self.portfolio_rc.need_adjust(portfolio)
|
||
current_drawdown = self.portfolio_rc.calculate_drawdown(portfolio)
|
||
current_total = sum(portfolio.positions.values())
|
||
current_ratio = current_total / portfolio.current_capital if portfolio.current_capital > 0 else 0
|
||
|
||
return {
|
||
# 止损结果
|
||
"has_stop_loss": len(stop_loss_list) > 0,
|
||
"stop_loss_stocks": stop_loss_list,
|
||
# 暴跌减仓结果
|
||
"has_crash_reduce": len(crash_reduce_list) > 0,
|
||
"crash_reduce_stocks": crash_reduce_list,
|
||
# 组合调整结果
|
||
"need_portfolio_adjust": need_portfolio_adjust,
|
||
"current_drawdown": current_drawdown,
|
||
"current_position_ratio": current_ratio,
|
||
"target_position_ratio": target_ratio
|
||
}
|
||
|
||
def get_risk_report(self, stocks: List[BreakoutStock], portfolio: PortfolioInfo) -> str:
|
||
"""生成风控日报"""
|
||
result = self.post_trade_check(stocks, portfolio)
|
||
|
||
lines = []
|
||
lines.append("=" * 50)
|
||
lines.append("纯突破策略 风控日报")
|
||
lines.append("=" * 50)
|
||
lines.append(f"初始资金: {portfolio.initial_capital:.0f}")
|
||
lines.append(f"当前资金: {portfolio.current_capital:.0f}")
|
||
lines.append(f"组合总回撤: {result['current_drawdown']:.2%}")
|
||
lines.append(f"当前仓位比例: {result['current_position_ratio']:.2%}")
|
||
lines.append(f"目标仓位比例: {result['target_position_ratio']:.2%}")
|
||
lines.append("")
|
||
|
||
if result['has_stop_loss']:
|
||
lines.append("⚠️ 需要止损个股(跌破突破低点5%):")
|
||
for item in result['stop_loss_stocks']:
|
||
lines.append(f" {item['code']} {item['name']} 突破低点:{item['breakout_low']:.2f} 当前:{item['current_price']:.2f} 跌幅:{item['drop_pct']:.2%}")
|
||
else:
|
||
lines.append("✅ 无个股触发跌破突破低点止损")
|
||
|
||
lines.append("")
|
||
if result['has_crash_reduce']:
|
||
lines.append("⚠️ 需要减仓个股(单日暴跌超7%):")
|
||
for item in result['crash_reduce_stocks']:
|
||
lines.append(f" {item['code']} {item['name']} 日跌幅:{item['daily_change']:.1f}% 当前持仓:{item['current_position']:.0f} → 目标:{item['target_position']:.0f}")
|
||
else:
|
||
lines.append("✅ 无个股触发单日暴跌减仓")
|
||
|
||
lines.append("")
|
||
if result['need_portfolio_adjust']:
|
||
lines.append(f"⚠️ 组合需要降仓调整,当前仓位高于目标")
|
||
else:
|
||
lines.append("✅ 组合仓位符合风控要求")
|
||
|
||
lines.append("=" * 50)
|
||
return "\n".join(lines)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 测试用例
|
||
print("=== 测试纯突破风控模块 ===")
|
||
|
||
# 测试1: 跌破突破低点止损
|
||
stock1 = BreakoutStock(
|
||
code="002123",
|
||
name="梦网集团",
|
||
breakout_date="2026-03-20",
|
||
breakout_low=20.0,
|
||
current_price=18.8,
|
||
daily_change=-3.2
|
||
)
|
||
sl = BreakoutSingleStopLoss()
|
||
triggered, drop = sl.check_stop_loss(stock1)
|
||
print(f"\n[测试1] 跌破突破低点止损: 突破低点20,当前18.8,跌幅{(drop*100):.1f}% → 触发={triggered}")
|
||
# 跌幅6%,应该触发5%止损 → True
|
||
|
||
# 测试2: 单日暴跌减仓
|
||
stock2 = BreakoutStock(
|
||
code="600123",
|
||
name="兰花科创",
|
||
breakout_date="2026-03-20",
|
||
breakout_low=15.0,
|
||
current_price=13.5,
|
||
daily_change=-7.8,
|
||
position_size=100000
|
||
)
|
||
crash = SingleDayCrashReduce()
|
||
triggered, change = crash.check_crash(stock2)
|
||
print(f"\n[测试2] 单日暴跌减仓: 日跌幅{change:.1f}% → 触发={triggered}")
|
||
print(f" 当前仓位100000 → 目标仓位{crash.get_target_position(100000)}")
|
||
|
||
# 测试3: 组合回撤控制
|
||
portfolio = PortfolioInfo(
|
||
initial_capital=1000000,
|
||
current_capital=880000,
|
||
positions={"002123": 200000, "600123": 150000}
|
||
)
|
||
pd_ctrl = PortfolioDrawdownControl()
|
||
dd = pd_ctrl.calculate_drawdown(portfolio)
|
||
need_adj, target = pd_ctrl.need_adjust(portfolio)
|
||
print(f"\n[测试3] 组合回撤控制: 初始100万,当前88万 → 回撤{dd:.1%}")
|
||
print(f" 需要调整={need_adj}, 目标仓位比例={target:.1%}")
|
||
|
||
# 测试4: 开仓过滤
|
||
filter_ = BreakoutPreTradeFilter()
|
||
st_stock = BreakoutStock(
|
||
code="000003",
|
||
name="ST基蛋",
|
||
breakout_date="2026-03-20",
|
||
breakout_low=10.0,
|
||
current_price=10.5,
|
||
daily_change=2.0,
|
||
is_st=True
|
||
)
|
||
ok, reason = filter_.filter_stock(st_stock)
|
||
print(f"\n[测试4] ST过滤: ok={ok}, reason={reason}")
|
||
|
||
liq_stock = BreakoutStock(
|
||
code="603xxx",
|
||
name="小票",
|
||
breakout_date="2026-03-20",
|
||
breakout_low=10.0,
|
||
current_price=10.5,
|
||
daily_change=1.5,
|
||
avg_daily_volume=0.3
|
||
)
|
||
ok, reason = filter_.filter_stock(liq_stock)
|
||
print(f"流动性过滤(日均0.3亿): ok={ok}, reason={reason}")
|
||
|
||
# 测试5: 整合风控报告
|
||
rc = BreakoutRiskController()
|
||
print("\n[测试5] 风控报告:")
|
||
print(rc.get_risk_report([stock1, stock2], portfolio))
|