22
33import logging
44
5- from quant_platform_kit .strategy_contracts import StrategyDecision
5+ from quant_platform_kit .strategy_contracts import PositionTarget , StrategyDecision
66
77logger = logging .getLogger (__name__ )
88
1111# ---------------------------------------------------------------------------
1212
1313
14+ def _position_weight (position : PositionTarget ) -> float | None :
15+ if position .target_weight is not None :
16+ return abs (float (position .target_weight ))
17+ return None
18+
19+
20+ def _reject_risk_gate (
21+ decision : StrategyDecision ,
22+ * ,
23+ risk_flag : str ,
24+ reason : str ,
25+ ) -> StrategyDecision :
26+ return StrategyDecision (
27+ positions = (),
28+ budgets = decision .budgets ,
29+ risk_flags = (* decision .risk_flags , risk_flag ),
30+ diagnostics = {
31+ ** (decision .diagnostics or {}),
32+ "risk_gate" : "REJECT" ,
33+ "reason" : reason ,
34+ },
35+ )
36+
37+
1438def apply_risk_gate (
1539 decision : StrategyDecision ,
1640 * ,
@@ -34,67 +58,58 @@ def apply_risk_gate(
3458 这个函数不可绕过 —— AGENTS.md 要求所有 entrypoint 必须调用。
3559 """
3660 positions = decision .positions or ()
37- risk_flags = list (decision .risk_flags or ())
3861
39- # 空仓放行(risk_off 场景)
4062 if not positions :
41- return decision
63+ return StrategyDecision (
64+ positions = decision .positions ,
65+ budgets = decision .budgets ,
66+ risk_flags = decision .risk_flags ,
67+ diagnostics = {** (decision .diagnostics or {}), "risk_gate" : "APPROVE" },
68+ )
4269
43- # 1. 集中度检查(默认不限制,由策略自行设定)
44- if max_single_weight < 1.0 :
45- for p in positions :
46- weight = abs (float (p .target_weight ))
70+ weight_positions = [p for p in positions if _position_weight (p ) is not None ]
71+
72+ if max_single_weight < 1.0 and weight_positions :
73+ for p in weight_positions :
74+ weight = _position_weight (p )
75+ assert weight is not None
4776 if weight > max_single_weight :
4877 logger .warning (
4978 "risk_gate REJECT concentration: symbol=%s weight=%.2f%% limit=%.0f%%" ,
5079 p .symbol , weight * 100 , max_single_weight * 100 ,
5180 )
52- return StrategyDecision (
53- positions = (),
54- risk_flags = ("rejected:concentration" ,),
55- diagnostics = {
56- ** (decision .diagnostics or {}),
57- "risk_gate" : "REJECT" ,
58- "reason" : f"{ p .symbol } { weight :.1%} > { max_single_weight :.0%} 上限" ,
59- },
81+ return _reject_risk_gate (
82+ decision ,
83+ risk_flag = "rejected:concentration" ,
84+ reason = f"{ p .symbol } { weight :.1%} > { max_single_weight :.0%} 上限" ,
6085 )
6186
62- # 2. 持仓数量检查
6387 if len (positions ) > max_positions :
6488 logger .warning (
6589 "risk_gate REJECT position_count: %d > %d" , len (positions ), max_positions ,
6690 )
67- return StrategyDecision (
68- positions = (),
69- risk_flags = ("rejected:too_many_positions" ,),
70- diagnostics = {
71- ** (decision .diagnostics or {}),
72- "risk_gate" : "REJECT" ,
73- "reason" : f"{ len (positions )} 个持仓 > { max_positions } 上限" ,
74- },
91+ return _reject_risk_gate (
92+ decision ,
93+ risk_flag = "rejected:too_many_positions" ,
94+ reason = f"{ len (positions )} 个持仓 > { max_positions } 上限" ,
7595 )
7696
77- # 3. 总仓位检查
78- total_weight = sum (abs (float (p .target_weight )) for p in positions )
79- if total_weight > max_total_exposure + 1e-9 :
80- logger .warning (
81- "risk_gate REJECT total_exposure: %.2f%% > %.0f%%" ,
82- total_weight * 100 , max_total_exposure * 100 ,
83- )
84- return StrategyDecision (
85- positions = (),
86- risk_flags = ("rejected:overexposed" ,),
87- diagnostics = {
88- ** (decision .diagnostics or {}),
89- "risk_gate" : "REJECT" ,
90- "reason" : f"总仓位 { total_weight :.1%} > { max_total_exposure :.0%} " ,
91- },
92- )
97+ if weight_positions :
98+ total_weight = sum (_position_weight (p ) or 0.0 for p in weight_positions )
99+ if total_weight > max_total_exposure + 1e-9 :
100+ logger .warning (
101+ "risk_gate REJECT total_exposure: %.2f%% > %.0f%%" ,
102+ total_weight * 100 , max_total_exposure * 100 ,
103+ )
104+ return _reject_risk_gate (
105+ decision ,
106+ risk_flag = "rejected:overexposed" ,
107+ reason = f"总仓位 { total_weight :.1%} > { max_total_exposure :.0%} " ,
108+ )
93109
94- # 通过
95- risk_flags .append ("risk_gate:passed" )
96110 return StrategyDecision (
97111 positions = decision .positions ,
98- risk_flags = tuple (risk_flags ),
112+ budgets = decision .budgets ,
113+ risk_flags = decision .risk_flags ,
99114 diagnostics = {** (decision .diagnostics or {}), "risk_gate" : "APPROVE" },
100115 )
0 commit comments