risk_manager.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. """
  2. 多维度风险管理体系 (Multi-Layer Risk Manager)
  3. 三层止损保护:
  4. 1. 单笔硬止损:-8%
  5. 2. 单日损失限制:-5%(减仓50%)
  6. 3. 账户熔断:-15%(清仓+冷静一周)
  7. 移动止盈:
  8. - 15%利润:保本
  9. - 30%利润:锁定15%
  10. - 10%回撤:止盈出场
  11. """
  12. from typing import Dict, Optional, List
  13. from dataclasses import dataclass, field
  14. from datetime import datetime, timedelta
  15. from enum import Enum
  16. class RiskStatus(Enum):
  17. """风险状态"""
  18. NORMAL = "normal"
  19. WARNING = "warning"
  20. COOLING = "cooling"
  21. CIRCUIT_BREAKER = "circuit_breaker"
  22. @dataclass
  23. class RiskCheckResult:
  24. """风险检查结果"""
  25. can_trade: bool
  26. should_close_position: bool
  27. should_reduce_position: bool
  28. reduction_pct: float
  29. status: RiskStatus
  30. message: str
  31. @dataclass
  32. class PositionRisk:
  33. """持仓风险状态"""
  34. symbol: str
  35. entry_price: float
  36. current_price: float
  37. highest_price: float
  38. position_size: float
  39. unrealized_pnl_pct: float
  40. drawdown_from_peak: float
  41. class MultiLayerRiskManager:
  42. """
  43. 多维度风险管理体系
  44. 实现三层止损 + 移动止盈 + 空仓机制
  45. """
  46. # 止损阈值
  47. SINGLE_STOP_PCT = 0.08 # 单笔-8%
  48. DAILY_LOSS_PCT = 0.05 # 单日-5%
  49. CIRCUIT_BREAKER_PCT = 0.15 # 账户-15%
  50. # 移动止盈阈值
  51. PROFIT_LOCK_1 = 0.15 # 15%保本
  52. PROFIT_LOCK_2 = 0.30 # 30%锁定15%
  53. TRAILING_STOP = 0.10 # 10%回撤止盈
  54. # 冷静期
  55. COOLING_DAYS = 5
  56. CIRCUIT_BREAKER_DAYS = 5
  57. def __init__(self):
  58. # 当前状态
  59. self.status = RiskStatus.NORMAL
  60. self.cooling_end_date: Optional[datetime] = None
  61. # 账户状态
  62. self.peak_equity = 0.0
  63. self.current_equity = 0.0
  64. self.daily_pnl = 0.0
  65. self.last_reset_date: Optional[datetime] = None
  66. # 持仓跟踪
  67. self.positions: Dict[str, PositionRisk] = {}
  68. # 历史记录
  69. self.risk_events: List[Dict] = []
  70. def initialize(self, initial_equity: float, current_date: datetime):
  71. """初始化风险管理体系"""
  72. self.peak_equity = initial_equity
  73. self.current_equity = initial_equity
  74. self.last_reset_date = current_date
  75. def update_equity(self, equity: float, current_date: datetime):
  76. """更新账户权益"""
  77. # 重置每日盈亏
  78. if self.last_reset_date != current_date:
  79. self.daily_pnl = 0.0
  80. self.last_reset_date = current_date
  81. # 计算当日盈亏
  82. self.daily_pnl = equity - self.current_equity
  83. self.current_equity = equity
  84. # 更新峰值
  85. if equity > self.peak_equity:
  86. self.peak_equity = equity
  87. def register_position(
  88. self,
  89. symbol: str,
  90. entry_price: float,
  91. position_size: float,
  92. current_date: datetime
  93. ):
  94. """注册新持仓"""
  95. self.positions[symbol] = PositionRisk(
  96. symbol=symbol,
  97. entry_price=entry_price,
  98. current_price=entry_price,
  99. highest_price=entry_price,
  100. position_size=position_size,
  101. unrealized_pnl_pct=0.0,
  102. drawdown_from_peak=0.0
  103. )
  104. def update_position(self, symbol: str, current_price: float):
  105. """更新持仓状态"""
  106. if symbol not in self.positions:
  107. return
  108. pos = self.positions[symbol]
  109. pos.current_price = current_price
  110. pos.highest_price = max(pos.highest_price, current_price)
  111. # 计算盈亏
  112. pos.unrealized_pnl_pct = (current_price - pos.entry_price) / pos.entry_price
  113. # 计算从峰值的回撤
  114. pos.drawdown_from_peak = (pos.highest_price - current_price) / pos.highest_price
  115. def check_risk(
  116. self,
  117. symbol: str,
  118. current_date: datetime
  119. ) -> RiskCheckResult:
  120. """
  121. 执行风险检查
  122. 按优先级检查:熔断 > 单日 > 单笔 > 止盈
  123. """
  124. # 1. 检查熔断状态
  125. if self.status == RiskStatus.CIRCUIT_BREAKER:
  126. if self.cooling_end_date and current_date < self.cooling_end_date:
  127. return RiskCheckResult(
  128. can_trade=False,
  129. should_close_position=True,
  130. should_reduce_position=False,
  131. reduction_pct=0.0,
  132. status=self.status,
  133. message="Circuit breaker active, trading halted"
  134. )
  135. else:
  136. # 熔断期结束,恢复
  137. self.status = RiskStatus.NORMAL
  138. self.cooling_end_date = None
  139. # 2. 检查账户熔断
  140. total_drawdown = (self.current_equity - self.peak_equity) / self.peak_equity
  141. if total_drawdown <= -self.CIRCUIT_BREAKER_PCT:
  142. self._trigger_circuit_breaker(current_date)
  143. return RiskCheckResult(
  144. can_trade=False,
  145. should_close_position=True,
  146. should_reduce_position=False,
  147. reduction_pct=0.0,
  148. status=self.status,
  149. message=f"Circuit breaker triggered: drawdown {total_drawdown:.2%}"
  150. )
  151. # 3. 检查单日损失
  152. daily_return = self.daily_pnl / self.current_equity if self.current_equity > 0 else 0
  153. if daily_return <= -self.DAILY_LOSS_PCT:
  154. return RiskCheckResult(
  155. can_trade=True,
  156. should_close_position=False,
  157. should_reduce_position=True,
  158. reduction_pct=0.5,
  159. status=RiskStatus.WARNING,
  160. message=f"Daily loss limit: {daily_return:.2%}"
  161. )
  162. # 4. 检查持仓风险
  163. if symbol in self.positions:
  164. pos = self.positions[symbol]
  165. # 单笔硬止损
  166. if pos.unrealized_pnl_pct <= -self.SINGLE_STOP_PCT:
  167. return RiskCheckResult(
  168. can_trade=True,
  169. should_close_position=True,
  170. should_reduce_position=False,
  171. reduction_pct=0.0,
  172. status=RiskStatus.WARNING,
  173. message=f"Single position stop: {pos.unrealized_pnl_pct:.2%}"
  174. )
  175. # 移动止盈
  176. trailing_check = self._check_trailing_stop(pos)
  177. if trailing_check:
  178. return trailing_check
  179. return RiskCheckResult(
  180. can_trade=True,
  181. should_close_position=False,
  182. should_reduce_position=False,
  183. reduction_pct=0.0,
  184. status=RiskStatus.NORMAL,
  185. message="Risk check passed"
  186. )
  187. def _check_trailing_stop(self, pos: PositionRisk) -> Optional[RiskCheckResult]:
  188. """检查移动止盈条件"""
  189. profit = pos.unrealized_pnl_pct
  190. # 盈利15%:保本
  191. if profit >= self.PROFIT_LOCK_1:
  192. # 检查是否回撤到成本价
  193. if pos.current_price <= pos.entry_price:
  194. return RiskCheckResult(
  195. can_trade=True,
  196. should_close_position=True,
  197. should_reduce_position=False,
  198. reduction_pct=0.0,
  199. status=RiskStatus.WARNING,
  200. message="Breakeven stop triggered"
  201. )
  202. # 盈利30%:锁定15%利润
  203. if profit >= self.PROFIT_LOCK_2:
  204. lock_price = pos.entry_price * (1 + self.PROFIT_LOCK_1)
  205. if pos.current_price <= lock_price:
  206. return RiskCheckResult(
  207. can_trade=True,
  208. should_close_position=True,
  209. should_reduce_position=False,
  210. reduction_pct=0.0,
  211. status=RiskStatus.WARNING,
  212. message="Profit lock stop triggered"
  213. )
  214. # 从峰值回撤10%
  215. if pos.drawdown_from_peak >= self.TRAILING_STOP:
  216. return RiskCheckResult(
  217. can_trade=True,
  218. should_close_position=True,
  219. should_reduce_position=False,
  220. reduction_pct=0.0,
  221. status=RiskStatus.WARNING,
  222. message=f"Trailing stop: {pos.drawdown_from_peak:.2%} from peak"
  223. )
  224. return None
  225. def _trigger_circuit_breaker(self, current_date: datetime):
  226. """触发熔断"""
  227. self.status = RiskStatus.CIRCUIT_BREAKER
  228. self.cooling_end_date = current_date + timedelta(days=self.CIRCUIT_BREAKER_DAYS)
  229. self.risk_events.append({
  230. "date": current_date,
  231. "type": "circuit_breaker",
  232. "drawdown": (self.current_equity - self.peak_equity) / self.peak_equity,
  233. "equity": self.current_equity
  234. })
  235. def close_position(self, symbol: str):
  236. """关闭持仓记录"""
  237. if symbol in self.positions:
  238. del self.positions[symbol]
  239. def should_stay_cash(
  240. self,
  241. scores: Dict[str, float],
  242. regime: str,
  243. winter_days: int,
  244. current_date: datetime
  245. ) -> bool:
  246. """
  247. 判断是否应空仓
  248. 条件:
  249. 1. 所有品种评分 < 60
  250. 2. Winter生态持续 > 30天
  251. 3. 当月已实现亏损 > 5%
  252. """
  253. # 检查评分
  254. all_below_threshold = all(score < 60 for score in scores.values())
  255. # 检查Winter持续时间
  256. extended_winter = (regime == "winter" and winter_days > 30)
  257. if all_below_threshold or extended_winter:
  258. return True
  259. return False
  260. def get_risk_summary(self) -> Dict:
  261. """获取风险摘要"""
  262. return {
  263. "status": self.status.value,
  264. "current_equity": self.current_equity,
  265. "peak_equity": self.peak_equity,
  266. "drawdown": (self.current_equity - self.peak_equity) / self.peak_equity,
  267. "daily_pnl": self.daily_pnl,
  268. "daily_return": self.daily_pnl / self.current_equity if self.current_equity > 0 else 0,
  269. "positions": len(self.positions),
  270. "cooling_end": self.cooling_end_date
  271. }