|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import logging |
| 4 | +from collections.abc import Mapping |
4 | 5 | from typing import Any |
5 | 6 |
|
| 7 | +from quant_platform_kit.risk.gate import apply_risk_gate as _qpk_apply_risk_gate |
| 8 | +from quant_platform_kit.risk.gate import enrich_decision_risk_diagnostics |
| 9 | +from quant_platform_kit.risk.portfolio_diagnostics import extract_portfolio_risk_diagnostics |
6 | 10 | from quant_platform_kit.strategy_contracts import PositionTarget, StrategyContext, StrategyDecision |
7 | 11 | from quant_platform_kit.strategy_lifecycle.performance_monitor import PerformanceMonitor |
8 | 12 |
|
@@ -40,89 +44,33 @@ def record_strategy_decision( |
40 | 44 | def apply_risk_gate( |
41 | 45 | decision: StrategyDecision, |
42 | 46 | *, |
| 47 | + ctx: StrategyContext | None = None, |
43 | 48 | max_single_weight: float = 1.0, |
44 | 49 | max_positions: int = 20, |
45 | 50 | max_total_exposure: float = 1.0, |
| 51 | + portfolio_snapshot: Any | None = None, |
| 52 | + market_data: Mapping[str, Any] | None = None, |
46 | 53 | ) -> StrategyDecision: |
47 | | - """对所有 StrategyDecision 施加硬风控门。 |
48 | | -
|
49 | | - 检查项: |
50 | | - 1. 单仓位集中度(> max_single_weight → REJECT,默认 100% 即不限制) |
51 | | - 2. 持仓数量(> max_positions → REJECT) |
52 | | - 3. 总仓位超限(> max_total_exposure → REJECT) |
53 | | -
|
54 | | - 各策略类型可根据自身特点调整门限: |
55 | | - - ETF 轮动:max_single_weight=1.0(ETF 本身就是分散的篮子) |
56 | | - - 个股精选:max_single_weight=0.10 |
57 | | - - 加密货币:max_single_weight=0.20, max_positions=10 |
58 | | -
|
59 | | - 如果 REJECT,返回空仓决策并标注拒绝原因。 |
60 | | - 这个函数不可绕过 —— AGENTS.md 要求所有 entrypoint 必须调用。 |
61 | | - """ |
62 | | - positions = decision.positions or () |
63 | | - risk_flags = list(decision.risk_flags or ()) |
64 | | - |
65 | | - # 空仓放行(risk_off 场景) |
66 | | - if not positions: |
67 | | - return decision |
68 | | - |
69 | | - # 1. 集中度检查(默认不限制,由策略自行设定) |
70 | | - if max_single_weight < 1.0: |
71 | | - for p in positions: |
72 | | - weight = abs(float(p.target_weight)) |
73 | | - if weight > max_single_weight: |
74 | | - logger.warning( |
75 | | - "risk_gate REJECT concentration: symbol=%s weight=%.2f%% limit=%.0f%%", |
76 | | - p.symbol, weight * 100, max_single_weight * 100, |
77 | | - ) |
78 | | - return StrategyDecision( |
79 | | - positions=(), |
80 | | - risk_flags=("rejected:concentration",), |
81 | | - diagnostics={ |
82 | | - **(decision.diagnostics or {}), |
83 | | - "risk_gate": "REJECT", |
84 | | - "reason": f"{p.symbol} {weight:.1%} > {max_single_weight:.0%} 上限", |
85 | | - }, |
86 | | - ) |
87 | | - |
88 | | - # 2. 持仓数量检查 |
89 | | - if len(positions) > max_positions: |
90 | | - logger.warning( |
91 | | - "risk_gate REJECT position_count: %d > %d", len(positions), max_positions, |
92 | | - ) |
93 | | - return StrategyDecision( |
94 | | - positions=(), |
95 | | - risk_flags=("rejected:too_many_positions",), |
96 | | - diagnostics={ |
97 | | - **(decision.diagnostics or {}), |
98 | | - "risk_gate": "REJECT", |
99 | | - "reason": f"{len(positions)} 个持仓 > {max_positions} 上限", |
100 | | - }, |
101 | | - ) |
102 | | - |
103 | | - # 3. 总仓位检查 |
104 | | - total_weight = sum(abs(float(p.target_weight)) for p in positions) |
105 | | - if total_weight > max_total_exposure + 1e-9: |
106 | | - logger.warning( |
107 | | - "risk_gate REJECT total_exposure: %.2f%% > %.0f%%", |
108 | | - total_weight * 100, max_total_exposure * 100, |
109 | | - ) |
110 | | - return StrategyDecision( |
111 | | - positions=(), |
112 | | - risk_flags=("rejected:overexposed",), |
113 | | - diagnostics={ |
114 | | - **(decision.diagnostics or {}), |
115 | | - "risk_gate": "REJECT", |
116 | | - "reason": f"总仓位 {total_weight:.1%} > {max_total_exposure:.0%}", |
117 | | - }, |
| 54 | + """QPK unified risk gate: stop-loss, circuit breaker, concentration (task 8).""" |
| 55 | + snapshot = portfolio_snapshot if portfolio_snapshot is not None else ( |
| 56 | + ctx.portfolio if ctx is not None else None |
| 57 | + ) |
| 58 | + if snapshot is not None: |
| 59 | + portfolio_diag = extract_portfolio_risk_diagnostics(snapshot) |
| 60 | + decision = enrich_decision_risk_diagnostics( |
| 61 | + decision, |
| 62 | + unrealized_pnl_pct=portfolio_diag.get("unrealized_pnl_pct"), |
| 63 | + consecutive_losses=portfolio_diag.get("consecutive_losses"), |
118 | 64 | ) |
119 | | - |
120 | | - # 通过 |
121 | | - risk_flags.append("risk_gate:passed") |
122 | | - return StrategyDecision( |
123 | | - positions=decision.positions, |
124 | | - risk_flags=tuple(risk_flags), |
125 | | - diagnostics={**(decision.diagnostics or {}), "risk_gate": "APPROVE"}, |
| 65 | + if market_data is None and ctx is not None: |
| 66 | + market_data = dict(ctx.market_data or {}) |
| 67 | + return _qpk_apply_risk_gate( |
| 68 | + decision, |
| 69 | + max_single_weight=max_single_weight, |
| 70 | + max_positions=max_positions, |
| 71 | + max_total_exposure=max_total_exposure, |
| 72 | + portfolio_snapshot=snapshot, |
| 73 | + market_data=market_data, |
126 | 74 | ) |
127 | 75 |
|
128 | 76 |
|
|
0 commit comments