Files
sanguo_quant_live/strategies/pure-breakout-20260327/risk_control_breakout.py
T

377 lines
14 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. 单票止损:跌破突破日最低价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))