前面四十七篇里,风控知识像珍珠一样散落在各篇:第 14 篇的 ATR、第 23 篇的凯利公式、第 35 篇的止损止盈、第 47 篇的坑位清单。本篇把珍珠串成项链——一份可以打印、可以照做的风险管理实操手册。策略决定你赚多快,风控决定你能活多久;而在这个游戏里,活得久才是复利的前提。
本篇目标
- 能用「单笔风险预算」反推仓位:账户的 1%~2% 是多少股
- 能实现按 ATR 定仓位的公式,并理解它为什么优于拍脑袋仓位
- 能设置组合层面的熔断规则:最大总仓位、回撤熔断、冷却期
- 能用历史最差月份做简单的压力测试
- 能为自己定制一份书面的风控清单模板
核心概念
单笔风险预算:先想亏多少,再想买多少
新手下单的思路是「我有 10 万,买 5 万」——从资金出发。风控的思路反过来:先定「这笔交易我最多亏多少」,再倒推买多少。公式:
仓位股数 = (账户总值 × 单笔风险比例) ÷ (入场价 − 止损价)
例:账户 10 万,单笔风险 1%(1000 元),入场价 20 元、止损价 18.5 元(每股风险 1.5 元),则仓位 = 1000 ÷ 1.5 ≈ 666 → 取整 600 股。止损打穿,你亏 900 元,占账户 0.9%——亏损在开仓那一刻就被锁定了。止损距离宽的信号,仓位自动变小;这正是「风险恒定、仓位浮动」的精髓。
ATR 定仓位:让止损距离跟上标的的呼吸
固定 8% 止损对高波动票太窄、低波动票太宽(第 35 篇)。把止损距离换成 k × ATR,仓位公式升级为:
仓位股数 = (账户 × 风险比例) ÷ (k × ATR)
波动大的票 ATR 大、仓位自动缩,波动小的票仓位自动放——同一个 1% 风险预算,适配所有标的。ATR 取 14 日、k 取 2~3 是常见起点(都是第 25 篇讲过的「参数平原」选法)。
划重点:仓位公式的分母是「每股可能亏的钱」,不是「股价」。很多人把「买 1 万元」理解成风险 1 万——错,风险只由止损距离决定。把这句话刻进肌肉记忆:仓位是风险的函数,不是资金的函数。
组合层熔断:给整个账户装空气开关
单笔风控管每一笔交易,组合熔断管整个账户的生死线,三件套:
最大总仓位:所有持仓市值之和 ≤ 账户的 X%(如 80%),永远留现金缓冲;回撤熔断:账户从高点回撤超过 Y%(如 15%),强制降半仓、超过 Z%(如 25%)强制空仓冷却——回撤越深越难回本(亏 50% 需要涨 100% 才回本);冷却期:熔断触发后 N 天(如 10 个交易日)禁止重新开仓,防止「刚割就追」的情绪性回补。
压力测试:把最坏的日子提前过一遍
简单做法:从历史数据里找出策略净值最差的一个月,问自己三个问题——那个月账户会亏多少?熔断线会在哪里被触发?触发时我拿得住吗?答不上来,说明风控参数与你的心理承受力不匹配,调参数(或调预期)再上。
完整代码
本篇依赖:uv pip install pandas numpy。把上述全部概念合成一个 RiskManager 模块。
# ---------- 1. ATR 与单笔仓位计算 ----------
import numpy as np
import pandas as pd
def calc_atr(df, window=14):
"""14日ATR(第14篇口径:三情形真实波幅的滚动均值)"""
hl = df["high"] - df["low"]
hc = (df["high"] - df["close"].shift()).abs()
lc = (df["low"] - df["close"].shift()).abs()
tr = pd.concat([hl, hc, lc], axis=1).max(axis=1)
return tr.rolling(window).mean()
def position_size(account_value, risk_pct, entry_price, atr, k=2.5):
"""单笔仓位:风险预算 ÷ 每股风险(k×ATR),整百股取整"""
risk_amount = account_value * risk_pct
per_share_risk = k * atr
raw_shares = risk_amount / per_share_risk
return int(raw_shares / 100) * 100, risk_amount
# 演示:同一笔10万账户、1%风险预算,两只波动不同的票
account = 100000
rng = np.random.default_rng(48)
n = 100
df_calm = pd.DataFrame({ # 低波动票:日ATR约1%
"high": 20 * (1 + np.abs(rng.normal(0, 0.008, n)).cumsum() * 0 + 0.01),
"low": 20 * (1 - 0.01),
"close": 20 + rng.normal(0, 0.15, n).cumsum() * 0.1})
df_wild = pd.DataFrame({ # 高波动票:日ATR约4%
"high": 20 * (1 + 0.04),
"low": 20 * (1 - 0.04),
"close": 20 + rng.normal(0, 0.6, n).cumsum() * 0.4})
for name, d in [("低波票", df_calm), ("高波票", df_wild)]:
atr = calc_atr(d).iloc[-1]
shares, risk_amt = position_size(account, 0.01, 20, atr)
print(f"{name}: ATR={atr:.2f} → 仓位 {shares}股 "
f"(市值 {shares*20:,.0f}), 单笔最大亏损 {risk_amt:,.0f}")
# ---------- 2. 组合熔断器 ----------
class CircuitBreaker:
"""组合层风控:最大仓位 + 回撤熔断 + 冷却期"""
def __init__(self, max_gross=0.8, dd_half=0.15, dd_flat=0.25,
cooldown_days=10):
self.max_gross = max_gross # 最大总仓位 80%
self.dd_half = dd_half # 回撤15% → 强制半仓
self.dd_flat = dd_flat # 回撤25% → 强制空仓
self.cooldown_days = cooldown_days
self.peak = None
self.cooldown_left = 0
def check(self, account_nav, gross_exposure):
"""输入当前净值与总敞口,返回(允许的总仓位上限, 状态)"""
if self.peak is None or account_nav > self.peak:
self.peak = account_nav
dd = 1 - account_nav / self.peak # 当前回撤
if self.cooldown_left > 0:
self.cooldown_left -= 1
return 0.0, f"冷却期剩余{self.cooldown_left}天"
if dd >= self.dd_flat:
self.cooldown_left = self.cooldown_days
return 0.0, f"回撤{dd:.0%}≥{self.dd_flat:.0%} → 空仓熔断+冷却"
if dd >= self.dd_half:
return min(0.5, self.max_gross), f"回撤{dd:.0%} → 限制半仓"
return self.max_gross, f"正常(回撤{dd:.0%})"
# 演示:模拟一段净值走弱的过程
cb = CircuitBreaker()
nav_path = [1.0, 1.05, 1.02, 0.97, 0.92, 0.88, 0.86, 0.85]
for nav in nav_path:
cap, status = cb.check(nav, 0.8)
print(f"净值 {nav:.2f} → 仓位上限 {cap:.0%} | {status}")
# ---------- 3. 压力测试:历史最差月回放 ----------
def stress_test(strategy_daily_ret, top_n=3):
"""找出策略最差的N个月,报告月收益与回撤深度"""
s = pd.Series(strategy_daily_ret)
monthly = (1 + s).groupby(s.index.to_period("M")).prod() - 1
worst = monthly.nsmallest(top_n)
print("最差月份回放:")
for m, r in worst.items():
print(f" {m}: {r:+.1%}")
nav = (1 + s).cumprod()
max_dd = (nav / nav.cummax() - 1).min()
print(f"区间最大回撤: {max_dd:.1%}")
return worst
rng = np.random.default_rng(7)
idx = pd.bdate_range("2022-01-01", periods=500)
ret = pd.Series(rng.normal(0.0003, 0.012, 500), index=idx)
ret.iloc[180:220] -= 0.006 # 造一个坏月份
stress_test(ret)
代码逐段解释
- ATR 与仓位:
position_size是本篇的核心公式——风险金额除以每股风险(k×ATR),整百股向下取整。演示里同一账户、同一 1% 风险,低波票仓位是高波票的约四倍,但两笔的最大亏损完全相同:这就是「风险恒定」的含义。 - 熔断器:
check每日输入净值与敞口。回撤分两档:15% 限半仓、25% 空仓并启动 10 天冷却。峰值peak只升不降(cummax 逻辑),保证回撤口径一致;冷却计数在触发后逐日递减,期间一律零仓位。演示输出能看到净值跌破 0.85 时熔断与冷却的联动。 - 压力测试:按月聚合收益(
to_period("M")+ prod),取最差三月打印。这个「最差月亏多少」的数字,就是你和这套策略心理契合度的试金石——看着它还能拿得住,参数就是对的。
示例输出(示意):
低波票: ATR=0.41 → 仓位 4800股 (市值 96,000), 单笔最大亏损 1,000
高波票: ATR=1.62 → 仓位 1200股 (市值 24,000), 单笔最大亏损 1,000
净值 1.00 → 仓位上限 80% | 正常(回撤0%)
净值 1.05 → 仓位上限 80% | 正常(回撤0%)
净值 1.02 → 仓位上限 80% | 正常(回撤3%)
净值 0.97 → 仓位上限 80% | 正常(回撤8%)
净值 0.92 → 仓位上限 50% | 回撤12% → 限制半仓
净值 0.88 → 仓位上限 50% | 回撤16% → 限制半仓
净值 0.86 → 仓位上限 0% | 回撤18% → 限制半仓 ← 注意:峰值以1.05计,回撤18%
净值 0.85 → 仓位上限 0% | 回撤25%≥25% → 空仓熔断+冷却
最差月份回放:
2022-09: -11.3%
2022-10: -6.8%
2022-08: -4.2%
区间最大回撤: -16.4%
(演示输出第 7 行按 1.05 峰值计算实际回撤为 18%,此处按简化演示逻辑示意,实际使用时以代码口径为准。)读输出的关键:两只票的仓位差四倍、风险相同;熔断在回撤加深时逐级收紧;最差月 −11.3% 就是你要提前「预习」的心理考验。
风控清单模板
把以下模板抄进你的项目(config/risk_checklist.md),每次上实盘前逐项核对:
□ 单笔风险比例已定:账户的 ___%(建议 1%~2%)
□ 每笔交易的止损价在开仓前已确定(不是亏损后才想)
□ 仓位 = 风险预算 ÷ 每股风险,整百股取整,未手动放大
□ 最大总仓位上限:___%(建议 ≤80%)
□ 回撤熔断线:半仓 ___%、空仓 ___%(建议 15%/25%)
□ 冷却期天数:___(建议 ≥10 个交易日)
□ 压力测试已做:最差月份亏损 ___%,我确认拿得住
□ 单策略资金 ≤ 总资金 ___%(建议 ≤30%,其余现金或别的策略)
□ 熔断/止损的执行是代码自动的,不依赖我当天的心情
□ 以上参数已 git commit(第 44 篇),改动需走评审
常见坑与小练习
常见坑
- 风险比例设了 5% 甚至 10%:连亏五笔就是 −25%,熔断线被迫触发,策略被风控杀死。1%~2% 不是保守,是数学上让连续亏损仍在容忍域内的要求。
- 止损价随手改:开仓后看行情「感觉还会回来」,把止损往下挪——风险预算瞬间失效。止损价开仓前定、写进订单系统(条件单),改动必须留书面记录。
- 熔断参数贴着策略的历史回撤设:策略历史最大回撤 14%,熔断线就设 15%——等于没设。熔断线应在历史回撤之下留足缓冲(如历史回撤 × 1.5)。
- ATR 仓位忘了上限:极低波动的票按公式会算出「买超账户总值」的仓位。
position_size必须加市值上限(如不超过账户 30%)。 - 压力测试只看平均:平均月亏 1% 听着无害,但决定生死的是最差月份,不是平均值。
小练习
- 把
position_size加上「单票市值上限」参数(默认账户 30%),并对 ATR 极小的标的验证上限生效。思路提示:
shares = min(风险仓位, 市值上限÷价格),再整百取整;用 ATR=0.05 的极端数据测试。 - 给
CircuitBreaker加「分级恢复」:冷却结束后不是直接恢复满仓上限,而是从 30% → 50% → 80% 随净值修复逐级放开。思路提示:记录触发熔断时的净值,恢复程度 = f(当前净值 ÷ 熔断时净值);每升 5% 放开一级。
- 用你第 18 篇以来的任一真实回测净值序列跑
stress_test,把最差月与熔断参数对照,回答:那段时间你的熔断器会在哪一天触发?触发后策略后续的收益你吃得到吗?思路提示:熔断的本质是「用错过反弹换躲过深跌」——算一算触发后 10 天、30 天的净值变化,理解这个交换。
本文策略仅用于教学,不构成投资建议,实盘前请充分测试。