量化风控止损策略仓位管理最大回撤Python风控模块

一次有色闪崩亏掉半年利润:用Python搭建量化实盘三层风控体系


6月23日收盘,洛阳钼业-9.98%,紫金矿业-8.84%。前一天还在涨停的洛阳钼业(6/22收盘21.65元),一天之内跌到19.49元。如果你在涨停那天追入满仓,一天亏掉的,可能需要三个月才能赚回来。

一场价值半年的闪崩

2026年6月,A股有色金属板块经历了一次教科书级的闪崩。先看数据:

标的6/22收盘6/23收盘单日跌幅
洛阳钼业21.6519.49-9.98%
紫金矿业30.4427.75-8.84%
山东黄金27.7924.76-10.90%

这还不是最惨的。拉长到20个交易日看指数回撤:

指数20日高点20日低点回撤幅度
创业板指4359.43811.2-12.57%
沪深3005059.74713.6-6.84%
上证503012.72825.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条信号追踪数据揭示了一个反直觉的规律:

市场环境信号数胜率平均收益
动量型-空头环境24050.0%+0.46%
动量型-多头环境34249.0%+0.62%
均值回归-空头环境17542.3%-0.31%
均值回归-多头环境8713.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%,冷却期结束后重新进场仍有足够机会。冷却期的核心目的不是”抓住反弹”,而是”避免在恐慌中犯错”。

相关文章推荐

💬 评论