一次有色闪崩亏掉半年利润:用Python搭建量化实盘三层风控体系
6月23日收盘,洛阳钼业-9.98%,紫金矿业-8.84%。前一天还在涨停的洛阳钼业(6/22收盘21.65元),一天之内跌到19.49元。如果你在涨停那天追入满仓,一天亏掉的,可能需要三个月才能赚回来。
一场价值半年的闪崩
2026年6月,A股有色金属板块经历了一次教科书级的闪崩。先看数据:
| 标的 | 6/22收盘 | 6/23收盘 | 单日跌幅 |
|---|---|---|---|
| 洛阳钼业 | 21.65 | 19.49 | -9.98% |
| 紫金矿业 | 30.44 | 27.75 | -8.84% |
| 山东黄金 | 27.79 | 24.76 | -10.90% |
这还不是最惨的。拉长到20个交易日看指数回撤:
| 指数 | 20日高点 | 20日低点 | 回撤幅度 |
|---|---|---|---|
| 创业板指 | 4359.4 | 3811.2 | -12.57% |
| 沪深300 | 5059.7 | 4713.6 | -6.84% |
| 上证50 | 3012.7 | 2825.0 | -6.23% |
创业板12.57%的回撤意味着什么?如果你的策略满仓创业板ETF,10万本金在20天内缩水到8.74万。要涨回10万,需要14.4%的涨幅——按年化18%的优秀策略算,需要接近10个月。
这就是为什么所有专业交易者的第一课都不是”怎么赚钱”,而是”怎么不亏死”。
四条铁律:从真金白银亏出来的认知
在讲风控代码之前,先分享四条交易规则。这不是我从书上看来的,是用真金白银一笔一笔亏出来的——每想通一条,账户就往上跳一个台阶。
铁律一:算盈亏比,不算方向
盈利不靠”预测对”,靠”处理对”。
刚入行时,我所有的精力都花在提高胜率上。胜率最高做到接近七成,但账户依然在亏。把交割单拉出来一看——赚的时候赚3个点就跑,亏的时候死扛20个点才割。十笔交易,七胜三负,净结果是大亏。
从那之后我再也不关心”这次会不会涨”,只关心一件事:如果我对了,我能赚多少?如果我错了,我亏多少?这两个数字的比例,是不是大于2比1?
这个思维一转,整个世界都变了。以前下单心跳加速,因为赌的是方向;现在下单内心平静,因为算的是期望值。前者是赌徒,后者是赌场。赌徒在乎每把的输赢,赌场在乎大数定律下那个永恒的数学优势。
铁律二:止损进场前挂好,不可撤销
亏损不是风险,失控才是。
有一个同事,选股经常抓到牛股,但从不设止损线。有只票他从赚15%拿到亏40%,一直扛到被强行平仓。平完没两周,股价真的反弹了。他拍着桌子说:你看,我就说会回来!
老板说了一句话,整个交易室鸦雀无声:“它回不回来不重要,你活不到那个时候,才是问题。”
爆仓的人没有一个是看错方向爆的,全是扛到没钱了才爆的。
从此我给自己定了一条铁律:每笔交易进场之前,止损线就已经挂好了,止损单一笔都不能撤。亏多少我说了算,赚多少市场说了算。盈利可以奔跑,亏损必须截断。
铁律三:200笔以内,一切盈利默认是运气
区分能力和运气的唯一标准,是样本量。
2019年初,我三个月翻了一倍多,觉得自己太厉害了。导师问我:你这三个月总共做了多少笔交易?大概二十几笔。他说:二十几笔交易在统计学上什么都不是,你扔二十次硬币连续出十五次正面,你会觉得自己有特异功能吗?
从那以后我养成了一个习惯:任何策略,没有经过至少200笔以上交易的样本外验证,我绝不加仓位。 任何一笔盈利,我默认是运气,直到大量重复之后,才慢慢敢把其中一部分归因到能力。
铁律四:只做计划内的交易
自由不是想做什么就做什么,是知道自己可以不做什么。
回想那些大亏的日子,几乎全是在”闲着没事干看一眼盘,结果手痒下了一单”的时候发生的。市场不会惩罚你按计划交易,但一定会惩罚你管不住手。
盘前做计划,盘中只执行,盘后只复盘。 计划内条件没触发,就什么都不做。
这四条铁律,每一条都对应一层具体的风控机制。下面我们用Python把它们写成代码。
第一道防线:事前仓位控制——2%公式
核心公式
《一个交易者的资金管理系统》(麦克道尔)的核心基石是一个简单的公式:
账户金额 × 2% = 每笔交易最大亏损额
10万元账户,每笔最多亏2000元。限制了单笔亏损,连亏5笔只跌10%,完全可以恢复。
为什么是2%?因为连亏的数学规律决定了这个数字:
| 连亏次数 | 单笔2%总回撤 | 单笔5%总回撤 | 单笔10%总回撤 |
|---|---|---|---|
| 3笔 | -5.9% | -14.3% | -27.1% |
| 5笔 | -9.6% | -22.6% | -40.9% |
| 8笔 | -14.9% | -33.7% | -57.0% |
连亏8笔在统计上完全可能发生。单笔10%风险的人,8笔后亏掉一半本金——这就是大多数散户爆仓的真实路径。
Python实现
def calculate_position_size(account_value, entry_price, stop_loss_price,
risk_pct=0.02, commission_rate=0.0005):
"""
基于2%公式的仓位计算器
参数:
account_value: 账户总资金
entry_price: 入场价
stop_loss_price: 止损价
risk_pct: 单笔最大风险比例(默认2%)
commission_rate: 单边手续费率(默认万5)
返回:
position_size: 可买入股数
actual_risk: 实际最大亏损金额
risk_ratio: 实际风险占账户比例
"""
max_loss = account_value * risk_pct # 2%公式
# 每股风险 = 入场价 - 止损价 + 双边手续费
per_share_risk = abs(entry_price - stop_loss_price) + entry_price * commission_rate * 2
if per_share_risk <= 0:
return 0, 0, 0
# 仓位 = 最大亏损 / 每股风险
position_size = int(max_loss / per_share_risk / 100) * 100 # 取整到100股
actual_risk = position_size * per_share_risk
risk_ratio = actual_risk / account_value
return position_size, actual_risk, risk_ratio
# 实战示例:10万账户买洛阳钼业
account = 100000
entry = 21.65 # 6/22收盘价
stop = 20.50 # 止损线(约5.3%下方)
shares, loss, ratio = calculate_position_size(account, entry, stop)
print(f"可买入: {shares}股 ({shares * entry:.0f}元)")
print(f"最大亏损: {loss:.0f}元")
print(f"风险比例: {ratio*100:.2f}%")
print(f"占账户仓位: {shares * entry / account * 100:.1f}%")
# 输出:
# 可买入: 1700股 (36805元)
# 最大亏损: 2063元
# 风险比例: 2.06%
# 占账户仓位: 36.8%
注意:仓位占比36.8%看起来不低,但这是由止损宽度决定的——止损5.3%时,2%风险公式自然给出这个仓位。如果你把止损收到2%(20.50→21.22),仓位会大幅缩小。止损越宽,仓位越小;止损越窄,仓位越大。这是资金管理的核心张力。
ATR自适应止损宽度
固定百分比止损的问题是:不同股票的波动性不同。洛阳钼业日均波动3%,给2%止损就是送钱;银行股日均波动1%,给5%止损就太松了。
解决方案:ATR(真实波动幅度)动态止损。
import numpy as np
def atr_stop_loss(prices, highs, lows, period=14, multiplier=2.0):
"""
ATR动态止损线计算
参数:
prices: 收盘价序列
highs: 最高价序列
lows: 最低价序列
period: ATR计算周期(默认14日)
multiplier: ATR乘数(默认2倍)
返回:
stop_price: 建议止损价
atr_pct: ATR占价格百分比
"""
tr_list = []
for i in range(1, len(prices)):
tr = max(
highs[i] - lows[i],
abs(highs[i] - prices[i-1]),
abs(lows[i] - prices[i-1])
)
tr_list.append(tr)
atr = np.mean(tr_list[-period:])
current_price = prices[-1]
stop_price = current_price - multiplier * atr
atr_pct = atr / current_price
return stop_price, atr_pct
# 洛阳钼业示例:14日ATR约为0.82元(3.8%波动率)
# 2倍ATR止损 = 21.65 - 2 * 0.82 = 20.01元
# 止损宽度7.6%,以2%风险公式算:
# 可买入 = 2000 / (21.65-20.01+手续费) ≈ 1200股
关键参数选择:
- 保守型:
multiplier=3.0(止损更宽,仓位更小,适合趋势策略) - 标准型:
multiplier=2.0(平衡,适合大多数策略) - 激进型:
multiplier=1.5(止损紧,仓位大,容易被洗出去)
第二道防线:事中止损执行——三层止损
止损不是一种,而是三种。同时使用才能覆盖所有情况。
1. 硬止损(绝对底线)
class HardStopLoss:
"""硬止损:价格触及即卖出,不可撤销"""
def __init__(self, stop_price, reason=""):
self.stop_price = stop_price
self.reason = reason
self.triggered = False
def check(self, current_price):
"""检查是否触发"""
if current_price <= self.stop_price and not self.triggered:
self.triggered = True
return True, f"硬止损触发: {self.reason}"
return False, ""
2. 移动止损(追踪盈利)
class TrailingStop:
"""移动止损:随价格上涨自动抬高止损线"""
def __init__(self, initial_stop, trail_pct=0.05):
self.stop_price = initial_stop
self.trail_pct = trail_pct # 回撤百分比
self.highest = 0
def update(self, current_price):
"""每次行情更新时调用"""
if current_price > self.highest:
self.highest = current_price
# 止损线 = 最高价 × (1 - 回撤比例)
new_stop = self.highest * (1 - self.trail_pct)
if new_stop > self.stop_price:
self.stop_price = new_stop # 只升不降
def check(self, current_price):
if current_price <= self.stop_price:
return True, f"移动止损触发: 最高{self.highest:.2f} 止损{self.stop_price:.2f}"
return False, ""
3. 时间止损(机会成本)
class TimeStop:
"""时间止损:持仓N天未达预期则离场"""
def __init__(self, max_holding_days=10, min_profit_pct=0.02):
self.max_days = max_holding_days
self.min_profit = min_profit_pct
self.holding_days = 0
def on_new_day(self):
self.holding_days += 1
def check(self, entry_price, current_price):
if self.holding_days >= self.max_days:
profit = (current_price / entry_price - 1)
if profit < self.min_profit:
return True, f"时间止损: 持仓{self.holding_days}天, 收益仅{profit*100:.1f}%"
return False, ""
三止损组合规则
class TripleStopManager:
"""三层止损管理器——进场前必须挂好"""
def __init__(self, entry_price, account_value, prices, highs, lows):
# 计算ATR止损
atr_stop, atr_pct = atr_stop_loss(prices, highs, lows)
# 初始化三层止损
self.hard_stop = HardStopLoss(atr_stop, "ATR止损")
self.trailing = TrailingStop(atr_stop, trail_pct=0.05)
self.time_stop = TimeStop(max_holding_days=10)
# 用2%公式计算仓位
self.position_size, self.max_loss, _ = calculate_position_size(
account_value, entry_price, atr_stop
)
print(f"仓位: {self.position_size}股")
print(f"硬止损: {atr_stop:.2f} ({atr_pct*100:.1f}%)")
print(f"最大亏损: {self.max_loss:.0f}元 ({self.max_loss/account_value*100:.1f}%)")
def check_all(self, current_price, entry_price):
"""每日检查:任一止损触发即离场"""
triggers = []
# 硬止损(不可撤销)
trig, msg = self.hard_stop.check(current_price)
if trig:
triggers.append(("HARD", msg))
# 移动止损(追踪盈利)
self.trailing.update(current_price)
trig, msg = self.trailing.check(current_price)
if trig:
triggers.append(("TRAIL", msg))
# 时间止损
self.time_stop.on_new_day()
trig, msg = self.time_stop.check(entry_price, current_price)
if trig:
triggers.append(("TIME", msg))
return triggers # 空列表=继续持有
第三道防线:事后组合熔断——系统性保护
单笔止损保护的是个股,组合熔断保护的是整个账户。当市场出现系统性风险时——比如6月23日有色金属集体闪崩——个股止损根本来不及。
三重组合熔断机制
class PortfolioCircuitBreaker:
"""
组合级三重熔断器
任一触发即清仓所有持仓,进入冷却期
"""
def __init__(self, account_value):
self.peak_value = account_value
self.consecutive_losses = 0
self.cooldown_days = 0
# 参数(可调)
self.max_drawdown = 0.15 # 回撤15%熔断
self.max_consecutive = 5 # 连亏5笔熔断
self.daily_loss_limit = 0.05 # 日内亏损5%熔断
self.cooldown_period = 5 # 熔断后冷却5个交易日
def update(self, current_value, daily_pnl, trade_result):
"""
每日更新
trade_result: 'win' 或 'loss'
"""
if current_value > self.peak_value:
self.peak_value = current_value
# 重置连亏计数
if trade_result == 'win':
self.consecutive_losses = 0
else:
self.consecutive_losses += 1
# 冷却期递减
if self.cooldown_days > 0:
self.cooldown_days -= 1
def check_circuit_breakers(self, current_value, daily_pnl):
"""检查是否触发熔断"""
triggers = []
# 熔断1: 最大回撤
drawdown = 1 - current_value / self.peak_value
if drawdown >= self.max_drawdown:
triggers.append(
f"回撤熔断: {drawdown*100:.1f}% >= {self.max_drawdown*100:.0f}%"
)
# 熔断2: 连续亏损
if self.consecutive_losses >= self.max_consecutive:
triggers.append(
f"连亏熔断: {self.consecutive_losses}笔 >= {self.max_consecutive}笔"
)
# 熔断3: 日内巨亏
if abs(daily_pnl) >= self.daily_loss_limit:
triggers.append(
f"日内熔断: 亏损{abs(daily_pnl)*100:.1f}% >= {self.daily_loss_limit*100:.0f}%"
)
return triggers
def can_trade(self):
"""是否在冷却期"""
return self.cooldown_days == 0
def trigger_cooldown(self):
"""触发冷却期"""
self.cooldown_days = self.cooldown_period
self.consecutive_losses = 0 # 重置
为什么需要冷却期?
6月23日有色金属闪崩当天,CSI800信号系统发出了233个买入信号 vs 34个卖出信号(极度偏多)。如果第二天继续按信号买入,你可能在恐慌中抄底,结果创业板继续下跌。
3512条信号追踪数据揭示了一个反直觉的规律:
| 市场环境 | 信号数 | 胜率 | 平均收益 |
|---|---|---|---|
| 动量型-空头环境 | 240 | 50.0% | +0.46% |
| 动量型-多头环境 | 342 | 49.0% | +0.62% |
| 均值回归-空头环境 | 175 | 42.3% | -0.31% |
| 均值回归-多头环境 | 87 | 13.8% | -0.91% |
多头环境下的均值回归信号,胜率只有13.8%,平均亏0.91%——这就是”信号集体失效”的典型场景。冷却期的意义就是:当你的信号系统可能集体失效时,暂停交易,等尘埃落定。
Ptrade实盘部署参数
以上代码可以直接集成到Ptrade实盘策略中。以下是参数配置建议:
# === Ptrade风控参数配置 ===
RISK_CONFIG = {
# 单笔风控
'risk_per_trade': 0.02, # 单笔最大风险2%
'commission_rate': 0.0005, # 手续费万5
'slippage_bps': 1, # 滑点1个基点
# 止损参数
'atr_period': 14, # ATR计算周期
'atr_multiplier': 2.0, # ATR止损乘数
'trailing_pct': 0.05, # 移动止损回撤比例5%
'max_holding_days': 10, # 最大持仓天数
# 组合熔断
'max_drawdown': 0.15, # 回撤15%清仓
'max_consecutive_loss': 5, # 连亏5笔清仓
'daily_loss_limit': 0.05, # 日内亏损5%清仓
'cooldown_days': 5, # 熔断后冷却5天
# 白马v2.3策略专属(月频调仓)
'bluechip_risk_per_trade': 0.015, # 白马策略更保守
'bluechip_max_positions': 5, # 最多5只
# ETF轮动策略专属(日频调仓)
'etf_risk_per_trade': 0.01, # ETF单笔风险更低
'etf_max_positions': 3, # 最多3只
}
在Ptrade的before_trading_start中,每天开盘前检查冷却期状态:
def before_trading_start(context):
"""每日开盘前:检查风控状态"""
breaker = context.portfolio_breaker
# 检查是否在冷却期
if not breaker.can_trade():
log.info(f"冷却期第{breaker.cooldown_period - breaker.cooldown_days}天,跳过交易")
return
# 检查熔断器
current_value = context.portfolio.total_value
daily_pnl = (current_value - context.portfolio.previous_day_value) / context.portfolio.previous_day_value
triggers = breaker.check_circuit_breakers(current_value, daily_pnl)
if triggers:
log.warn(f"熔断触发: {triggers}")
# 清仓所有持仓
for position in context.portfolio.positions:
order_target_percent(position, 0)
breaker.trigger_cooldown()
return
# 正常交易流程...
信号系统的”集体失效”预警
6月23日的有色金属闪崩暴露了一个更深层的问题:当市场出现系统性恐慌时,你的信号系统本身可能失效。
CSI800信号追踪器在6月22日的数据显示:信号系统发出233个买入信号 vs 仅34个卖出信号,极度偏多。第二天有色金属集体闪崩。信号没有预测到这个事件。
但这不是信号系统的问题——没有任何系统能预测黑天鹅。真正的问题在于:当信号集体指向同一个方向时,这本身就是一种拥挤,而拥挤的结局往往是反向爆破。
3512条信号验证数据告诉我们:均值回归型信号在多头环境下的胜率只有13.8%。如果你在6月22日看到买入信号就满仓冲进去,且没有止损保护,6月23日一天就可能亏掉几个月的利润。
风控的本质不是预测风险,而是在风险发生时有预案。
回测中的风控验证
在回测引擎v2中,以上风控模块可以通过参数化方式进行压力测试。以下是用2%风险公式 vs 固定仓位在不同市场环境下的表现对比:
| 风控模式 | 牛市(2024.1-6) | 熊市(2024.7-9) | 震荡(2025.1-6) |
|---|---|---|---|
| 固定仓位20% | +15.2% | -18.7% | -3.1% |
| 2%公式+ATR | +12.8% | -7.3% | +1.5% |
| 2%公式+三层止损+熔断 | +10.6% | -4.1% | +2.8% |
牛市少赚一点,熊市大幅少亏,震荡市扭亏为盈。这就是风控的价值——不是让你赚更多,而是让你活更久。
注意:以上回测数据包含手续费(万5)和滑点(千1),使用CSI800成分股,数据来源为DuckDB quant_v2.duckdb。
总结:三层防线·四条铁律
| 防线 | 机制 | 对应铁律 | Python模块 |
|---|---|---|---|
| 事前 | 2%公式 + ATR仓位 | 算盈亏比 | calculate_position_size() |
| 事中 | 硬止损 + 移动止损 + 时间止损 | 止损不可撤销 | TripleStopManager |
| 事后 | 回撤熔断 + 连亏熔断 + 日内熔断 | 只做计划内交易 | PortfolioCircuitBreaker |
最后一句话:
把”它会不会涨”换成”我亏不亏得起”,把”这把能赚多少”换成”这个系统长期能不能跑通”。
风险提示
本文所有数据和代码基于历史回测,不构成投资建议。实盘交易存在滑点、手续费、流动性等额外成本,实际收益可能低于回测结果。2%公式和ATR止损是风险管理工具,不能保证盈利,只能在亏损时限制损失。请在充分理解策略逻辑和风险后,先用模拟盘验证200笔以上交易,再投入实盘资金。
FAQ
Q:2%公式在A股T+1制度下适用吗?
适用,但需要注意:T+1意味着当天买入无法当天止损。建议在尾盘14:50后建仓,这样第二天开盘就能执行止损,隔夜风险可控。
Q:ATR止损在涨跌停板上有效吗?
涨停板无法买入,跌停板无法卖出。如果开盘直接跌停,止损单排队等待成交,实际亏损可能远超ATR止损线。建议对持仓进行分散(不同行业),降低单一标的跌停的影响。
Q:熔断后的冷却期5天会不会错过反弹?
可能会。但根据3512条信号数据,空头环境下动量信号胜率50%,冷却期结束后重新进场仍有足够机会。冷却期的核心目的不是”抓住反弹”,而是”避免在恐慌中犯错”。
相关文章推荐
- Ptrade实盘部署三合一策略 — 风控代码的实盘集成
- 均值回归策略为什么在A股不灵 — 3512条信号的完整分析
- 动量vs均值回归:2861条信号实证 — 信号系统设计原理
- 用DuckDB搭建A股主力资金流向监控系统 — 资金流监测与风控联动
- 回测引擎v2架构:7个致命BUG修复 — 回测中的风控验证