9. 仓位与风险管理

9.仓位与风险管理

交易世界里,仓位大小不是拍脑袋决定的数字,而是风险管理的第一道防线。很多人回测时业绩漂亮,实盘却节节败退,问题往往出在这里——用固定仓位跑回测,却忽略了市场波动和账户资金的动态变化。

Jesse 框架把风险管理工具直接嵌入了策略 API,让我们能在代码层面就把风控逻辑写死,避免盘中情绪干扰。这一章要讲的,就是如何把抽象的风险偏好,翻译成具体的仓位数字,以及在持仓过程中如何动态调整止损止盈,守住利润底线。

风险转仓位计算

理解 risk_to_qty 的核心逻辑

量化交易的核心原则之一:先确定愿意亏多少钱,再决定开多少仓位。utils.risk_to_qty 这个函数就是干这个的,它可能是整个框架里最重要的工具函数。很多人第一次接触时觉得参数多,其实逻辑非常直观——给定账户资金、单笔交易风险比例、入场价和止损价,它自动算出应该开多少数量。

函数签名看起来参数不少:

risk_to_qty(capital, risk_per_capital, entry_price, stop_loss_price, precision=3, fee_rate=0)

前四个是核心。capital 是账户可用资金,risk_per_capital 是每笔交易愿意承担的风险百分比,比如 1 就代表 1%。entry_price 和 stop_loss_price 决定了每单位仓位的潜在亏损。函数内部计算逻辑很简单:先算出入场和止损之间的价差风险,再用 capital * risk_per_capital 得出总可亏损金额,最后除以单位风险得到数量。

看个具体例子。账户有 10,000 USDT,策略规定每笔交易最多亏 1%,入场价 100 USDT,止损设在 80 USDT。这意味着每单位仓位可能亏 20 USDT,而总可亏损预算是 100 USDT,所以最多开 5 个单位。代码写起来很直接:

def go_long(self):
    entry = 100
    stop = 80
    profit = 150
    risk_perc = 1  # 1% 风险
    
    qty = utils.risk_to_qty(self.balance, risk_perc, entry, stop)
    
    self.buy = qty, entry
    self.stop_loss = qty, stop
    self.take_profit = qty, profit

这里用 self.balance 动态获取当前账户权益,而不是写死 10000,这样策略在复利模式下会自动调整仓位大小。precision 参数控制数量的小数位数,默认 3 位对大多数币够用。fee_rate 参数必须重视,后面会专门讲。

需要警惕的是止损价离入场价太近的情况。假设止损设在 99 USDT,每单位风险只有 1 USDT,按 1% 风险预算算出来要开 100 个单位,可能超出账户实际购买力。这不是函数 bug,而是数学公式的必然结果。文档里特别提醒这种情况,止损过近会导致计算出的仓位超过可用资金,触发异常。实际策略中必须加保护逻辑,后面会讲如何设置仓位上限。

手续费对仓位计算的影响

实盘交易里,手续费是隐形的仓位杀手。计算仓位时如果不考虑手续费,很容易出现"钱不够"的错误。Jesse 在撮合时会严格检查购买力,如果 qty * entry_price + fee > capital,会直接抛异常阻止下单。

risk_to_qty 的 fee_rate 参数就是解决这个问题的。把交易所的手续费率传进去,函数会在计算时预留出手续费空间。假设现货交易手续费 0.1%,上面的例子应该改成:

qty = utils.risk_to_qty(self.balance, risk_perc, entry, stop, fee_rate=self.fee_rate)

self.fee_rate 是策略内置属性,自动读取配置里的手续费设置。加了这参数后,算出的数量会稍微少一点,确保下单时加上手续费也不会超支。别小看这千分之一的差别,在高频交易或者小资金账户里,可能就是成功下单和被拒单的区别。

回测时建议总是加上 fee_rate,让测试结果更接近实盘。很多人回测曲线很漂亮,实盘却做不出来,差距往往在这些细节里。手续费不仅影响仓位,长期看对策略夏普比率的影响更大,属于必须考虑的现实摩擦成本。

凯利准则在仓位管理中的应用

凯利准则(Kelly Criterion)是资金管理领域的经典公式,告诉我们在已知胜率和盈亏比的情况下,最优的风险比例是多少。Jesse 提供了 utils.kelly_criterion 函数,可以直接算出这个比例。

函数需要两个参数:win_rate 是历史胜率,比如 0.55 代表 55% 的胜率;ratio_avg_win_loss 是平均盈利与平均亏损的比值。返回值是一个百分比,建议投入的资金比例。

实际策略中,我们通常在 metrics 属性里获取历史数据。metrics 字典包含了回测的所有统计指标,当交易次数超过一定阈值后(比如 20 次),就可以用真实数据计算凯利值:

def kelly_qty(self, entry, stop):
    # 交易次数不足时用保守估计
    if not self.metrics or self.metrics['total'] < 20:
        win_rate = 0.46
        avg_win_ratio = 1.1
        avg_loss_ratio = 0.5
    else:
        win_rate = self.metrics['win_rate']
        avg_win_ratio = self.metrics['avg_win_percentage'] / 100
        avg_loss_ratio = self.metrics['avg_loss_percentage'] / 100
    
    kc = utils.kelly_criterion(win_rate, avg_win_ratio, avg_loss_ratio) * 100
    
    if not kc or kc <= 0:
        raise ValueError("凯利准则计算异常")
    
    # 用凯利比例计算风险仓位
    risk_qty = utils.risk_to_qty(self.available_margin, kc, entry, stop, self.fee_rate)
    
    # 限制最大风险不超过 25%
    max_qty = utils.size_to_qty(0.25 * self.available_margin, entry, precision=6, fee_rate=self.fee_rate)
    
    return min(risk_qty, max_qty)

这段代码展示了完整的风控思维。首先判断交易样本是否足够,不够时用保守的预设值。然后计算凯利比例,但凯利值可能为负(当系统期望为负时),需要检查有效性。最后用凯利比例代替固定风险比例调用 risk_to_qty,但同时设置了 25% 的资金上限,防止凯利值过大导致仓位过重。

凯利准则的争议在于它假设胜率和盈亏比稳定,而市场结构会变化。所以实际使用中,很多人会采用"半凯利"或"四分之一凯利",也就是把算出的比例再砍半或砍到四分之一。上面的代码用 25% 资金上限,相当于对凯利值做了二次约束。

设置仓位上限防止过度风险

前面多次提到止损过近会导致仓位过大,现在看具体解决方案。核心思路是同时用两种方法计算仓位,取较小值。一种按风险比例算,另一种按最大资金占用比例算。

utils.size_to_qty 函数把资金金额转换成数量。比如账户 25% 的资金是 2500 USDT,入场价 100 USDT,就能买 25 个单位。结合前面的风险计算:

@property
def position_size(self, entry, stop):
    # 按 10% 风险预算计算
    risk_qty = utils.risk_to_qty(self.balance, 10, entry, stop, fee_rate=self.fee_rate)
    
    # 按 25% 资金占用计算
    max_qty = utils.size_to_qty(0.25 * self.balance, entry, precision=6, fee_rate=self.fee_rate)
    
    # 取较小值,确保既不超风险预算,也不超资金上限
    qty = min(risk_qty, max_qty)
    return qty

这个 position_size 方法可以作为策略的通用属性,在 go_long 和 go_short 里直接调用。10% 的风险比例在加密货币这种高波动市场都算比较激进,传统市场通常用 1-2%。25% 的资金占用上限也是为了防止过度集中,具体数值可以根据策略特性和风险偏好调整。

这种双重约束的思路值得推广。量化风控的本质就是层层设限,避免单一规则失效导致灾难。就像给仓位套上两个紧箍咒,哪个更紧就听哪个的,确保最坏情况下损失可控。

动态仓位调整

在策略中动态计算入场仓位

实际策略很少用固定止损价,通常基于技术指标动态计算。布林带是常见工具,下轨止损、上轨止盈,通道宽度自动适应市场波动。看一个完整例子:

def go_long(self):
    # 用布林带下轨止损,上轨止盈
    stop = self.bb.lowerband
    take_profit = self.bb.upperband
    
    # 动态计算仓位
    qty = self.position_size(self.price, stop)
    
    self.buy = qty, self.price
    self.stop_loss = qty, stop
    self.take_profit = qty, take_profit

这里 self.bb 是布林带指标对象,假设在 before() 里已经计算好。self.price 是当前价格,self.position_size 是前面定义的仓位计算方法。整个过程一气呵成:先确定止损位,再算仓位,最后下单。止损位动态变化,仓位也跟着动态调整,市场波动大时通道变宽,止损距离拉大,仓位自动缩小,完全符合"波动越大仓位越小"的风控原则。

这种模式可以套用到任何技术指标。ATR 止损、支撑阻力位止损、移动平均线止损,逻辑都一样:先确定止损点,再算仓位,最后下单。关键是把仓位计算封装成独立方法,保持 go_long 的简洁。

根据账户权益调整风险敞口

复利是量化策略长期收益的核心。要让策略自动复利,必须在每次计算仓位时用当前账户权益,而不是初始资金。self.balance 和 self.available_margin 就是干这个的。

self.balance 是总权益,self.available_margin 是可用保证金。现货策略用 self.balance 就够了,合约策略要注意区别:已占用的保证金不能重复开仓。看一个合约策略的例子:

def go_long(self):
    # 用可用保证金计算,避免过度占用
    capital = self.available_margin
    
    # 止损设在入场价下方 2 倍 ATR
    stop = self.price - ta.atr(self.candles) * 2
    
    # 按 2% 风险计算
    qty = utils.risk_to_qty(capital, 2, self.price, stop, fee_rate=self.fee_rate)
    
    self.buy = qty, self.price
    self.stop_loss = qty, stop

这里用 self.available_margin 确保不会超出保证金限制。ATR 止损让止损距离随市场波动自适应。2% 的风险比例相对保守,适合多策略组合运行。随着账户盈利,available_margin 增加,算出的仓位自动变大;亏损时自动缩小,实现机械式复利。

有人担心这样会导致亏损后仓位缩小,回本变慢。这恰恰是凯利准则和风险管理的核心:亏损时降低风险,保护本金;盈利时适当增加风险,让利润奔跑。如果反过来,亏损后加大仓位想快速回本,那就是赌徒心态,迟早会爆仓。

仓位调整的最佳实践

动态仓位调整有几个坑要注意。首先是止损距离过近的问题,前面讲过。其次是手续费和滑点的累积效应,频繁调整仓位可能导致摩擦成本吃掉利润。最后是策略逻辑的一致性,不能为了放大收益而随意提高风险比例。

一个实用的模式是在策略配置里定义风险参数:

class MyStrategy(Strategy):
    def __init__(self):
        super().__init__()
        self.risk_per_trade = 1  # 每笔交易风险 1%
        self.max_capital_per_trade = 25  # 单笔最大占用资金 25%

然后在仓位计算时引用这些配置:

@property
def position_size(self, entry, stop):
    risk_qty = utils.risk_to_qty(
        self.balance, 
        self.risk_per_trade, 
        entry, 
        stop, 
        fee_rate=self.fee_rate
    )
    
    max_qty = utils.size_to_qty(
        self.max_capital_per_trade / 100 * self.balance, 
        entry, 
        precision=6, 
        fee_rate=self.fee_rate
    )
    
    return min(risk_qty, max_qty)

这样调整策略参数时不用改核心逻辑,也方便做参数优化。risk_per_trade 和 max_capital_per_trade 可以加入第 10 章的优化参数列表,寻找最佳风险配置。

另一个实践是区分策略类型。趋势跟踪策略胜率低但盈亏比高,风险比例可以设小一点,比如 0.5%;均值回归策略胜率高但盈亏比低,可以适当提高到 1-2%。这种差异化配置能让组合更稳健。

策略止损管理

设置初始止损

止损是交易员的救生索。Jesse 里设置止损很简单,在 go_long 或 go_short 里赋值 self.stop_loss 即可。它接受两个参数:数量和价格。

def go_long(self):
    qty = 1
    entry = self.price
    stop = self.price - 2  # 止损 2 美元
    
    self.buy = qty, entry
    self.stop_loss = qty, stop

数量和入场数量保持一致,价格就是止损位。系统会在下单后自动提交一个止损订单。对于多头,止损价必须低于入场价;空头则相反。如果方向搞反,Jesse 会抛异常阻止,避免低级错误。

止损价可以是固定数值,也可以是技术指标。常用的是 ATR 止损:

def go_long(self):
    qty = self.position_size(self.price, self.price - ta.atr(self.candles) * 1.5)
    self.buy = qty, self.price
    self.stop_loss = qty, self.price - ta.atr(self.candles) * 1.5

这里用 1.5 倍 ATR 作为止损距离,让止损幅度适应市场波动。波动大的品种,ATR 值大,止损距离自动放宽,避免被正常波动扫掉;波动小的品种,止损更紧凑,提高资金效率。

移动止损到盈亏平衡点

持仓浮盈后,把止损移到成本价是保护利润的基本操作。Jesse 在 update_position 里可以动态修改止损。

def update_position(self):
    # 当盈利超过 2 倍 ATR 时,移动到盈亏平衡
    if self.is_long and self.price > self.position.entry_price + ta.atr(self.candles) * 2:
        self.stop_loss = self.position.qty, self.position.entry_price

self.is_long 判断当前是多头持仓,self.position.entry_price 是开仓均价。当价格涨到均价上方 2 倍 ATR 时,把止损价设为均价。这样即使价格回调触发止损,也只是平推出场,不会亏损本金。

移动止损的时机很关键。设得太早,容易被小幅回调扫掉,错过后续行情;设得太晚,可能利润大幅回吐。用 ATR 倍数作为触发条件是比较科学的方法,让利润垫足够厚时再移动。

对于空头逻辑相反:

if self.is_short and self.price < self.position.entry_price - ta.atr(self.candles) * 2:
    self.stop_loss = self.position.qty, self.position.entry_price

使用技术指标作为动态止损

Jesse 内置了几个专门用于止损的指标,比如 safezonestop、devstop、kaufmanstop。这些在第 7 章会详细讲,这里只看如何应用。

def go_long(self):
    # 使用 devstop 作为动态止损
    stop = self.devstop.lowerband
    qty = self.position_size(self.price, stop)
    
    self.buy = qty, self.price
    self.stop_loss = qty, stop

devstop 是偏离止损,根据价格波动自动调整止损位。类似地,通道指标如布林带、肯特纳通道、唐奇安通道的上下轨也可以做止损:

# 布林带下轨止损
self.stop_loss = qty, self.bb.lowerband

# 肯特纳通道下轨止损
self.stop_loss = qty, self.kc.lowerband

# 唐奇安通道下轨止损(用 20 周期低点)
self.stop_loss = qty, self.dc.lowerband

这些指标的共同特点是让止损位随市场波动自适应,避免固定止损在波动率变化时失效。具体用哪个取决于策略逻辑,趋势策略可能用唐奇安通道,震荡策略可能用布林带。

限制止损距离防止异常

有时候技术指标会给出不合理的止损位,比如止损价离入场价太远,导致实际风险远超预算。utils.limit_stop_loss 可以强制限制止损距离。

def go_long(self):
    # 原始止损设在支撑位
    raw_stop = self.support_level
    
    # 限制最大风险不超过 5%
    stop = utils.limit_stop_loss(
        entry_price=self.price,
        stop_price=raw_stop,
        trade_type='long',
        max_allowed_risk_percentage=5
    )
    
    qty = utils.risk_to_qty(self.balance, 2, self.price, stop)
    self.buy = qty, self.price
    self.stop_loss = qty, stop

如果 raw_stop 导致的风险超过 5%,函数会自动调整止损价,让风险恰好等于 5%。这在用支撑位止损时很有用,有时候支撑位离得很远,如果不加限制,算出的仓位会极小,策略效率低下。限制最大风险能确保每笔交易至少占用一定资金。

策略止盈管理

设置初始止盈目标

止盈和止损逻辑对称,在 go_long 里设置 self.take_profit:

def go_long(self):
    qty = 1
    entry = self.price
    profit = self.price + 5  # 止盈 5 美元
    
    self.buy = qty, entry
    self.take_profit = qty, profit

止盈价对多头必须高于入场价,空头则相反。设置止盈的本质是确定风险回报比。如果止损 2 美元,止盈 5 美元,风险回报比就是 1:2.5。一般策略要求至少 1:2 以上,否则长期难以覆盖亏损。

技术指标止盈更常见:

def go_long(self):
    # 用布林带上轨止盈
    profit = self.bb.upperband
    qty = self.position_size(self.price, self.bb.lowerband)
    
    self.buy = qty, self.price
    self.stop_loss = qty, self.bb.lowerband
    self.take_profit = qty, profit

这样入场时就知道,止损在布林带下轨,止盈在上轨,通道宽度决定了风险回报比。震荡策略常用这种模式,在通道内高抛低吸。

动态调整止盈位

趋势行情里,固定止盈会过早下车。动态止盈让利润奔跑。在 update_position 里可以修改止盈价:

def update_position(self):
    # 用肯特纳通道上轨作为移动止盈
    if self.is_long:
        self.take_profit = self.position.qty, self.kc.upperband

每次 K 线更新时,止盈位都会重新计算。如果通道上移,止盈价也跟着上移,锁定更多利润。这种追踪止盈在趋势策略中几乎是标配。

更激进的做法是用抛物线转向指标(SAR)作为止盈:

def update_position(self):
    if self.is_long:
        self.take_profit = self.position.qty, self.sar.sar

SAR 的特点是加速追赶价格,能更快锁定利润,但也更容易被小幅回调触发。选择哪种止盈方式,取决于策略对盈亏比的偏好。

分批止盈策略

有时候我们想分批次平仓,比如达到第一目标后平一半,剩下的用移动止盈。这涉及到多点进出管理,属于第 6 章的高级内容。这里简单提一下思路:

def update_position(self):
    # 如果达到第一目标且还没平过半仓
    if self.price >= self.first_target and not self.half_exited:
        # 平一半
        self.take_profit = self.position.qty / 2, self.price
        self.half_exited = True
    
    # 剩下的仓位用移动止盈
    if self.half_exited:
        self.take_profit = self.position.qty, self.trailing_stop

这种模式能平衡落袋为安和让利润奔跑的矛盾。具体实现需要配合 on_close 钩子记录状态,这里不展开,避免越界到第 6 章。

强平价格监控

理解标记价格与强平机制

合约交易特有的概念。self.mark_price 返回标记价格,永续合约用它来计算未实现盈亏和强平价格。回测时它等于 self.price,实盘时由交易所提供,通常基于指数价格,防止被恶意操纵。

监控强平价格的意义在于提前预警。虽然 Jesse 会自动处理强平,但被动强平通常意味着巨大亏损。我们可以在 update_position 里计算强平价与当前价的距离:

def update_position(self):
    if self.is_long:
        # 假设交易所强平规则是保证金率低于 0.5%
        liquidation_price = self.position.entry_price * (1 - self.available_margin / self.position.value * 0.5)
        
        # 如果距离强平价不到 1%,主动减仓
        if self.price < liquidation_price * 1.01:
            self.log('接近强平价,主动平仓')
            self.liquidate()

这段代码是简化示例,实际强平计算涉及维持保证金率、手续费等复杂因素,不同交易所规则不同。关键是理念:主动风控优于被动强平。

资金费率的影响

self.funding_rate 和 self.next_funding_timestamp 是永续合约特有的属性。资金费率是多头和空头之间的利息交换,用于让合约价格锚定现货。费率正时多头付给空头,负时相反。

虽然资金费率不直接改变仓位大小,但影响持仓成本。高频策略需要考虑资金费率对收益的影响,特别是在费率波动大的市场。可以在 update_position 里检查即将到来的资金费率:

def update_position(self):
    # 如果资金费率很高且即将支付,考虑提前平仓
    if self.funding_rate > 0.001 and self.is_long:
        time_to_funding = self.next_funding_timestamp - self.time
        if time_to_funding < 3600:  # 1 小时内
            self.log('资金费率过高,提前平仓')
            self.liquidate()

这种逻辑适合短线策略,避免被高额资金费率侵蚀利润。长线策略通常忽略资金费率,因为趋势收益远大于费率成本。

主动平仓与强制平仓

self.liquidate() 是快速平仓的快捷方法。它会根据持仓方向自动提交市价单,多头时平多,空头时平空。在 update_position 里,当满足特定条件时调用:

def update_position(self):
    # 自定义出场逻辑,比如时间退出
    if self.index - self.position.entry_index > 100:
        self.liquidate()

这段代码在持仓超过 100 根 K 线后强制平仓,属于时间止损。liquidate() 内部等价于:

if self.position.pnl > 0:
    self.take_profit = self.position.qty, self.price
else:
    self.stop_loss = self.position.qty, self.price

它会立即以市价平仓,不保证价格。适合紧急情况,比如策略检测到异常市场结构,或者达到最大持仓时间。

与止损的区别在于,止损是预设的价格位,而 liquidate() 是即时执行。止损可能被触发在不利价格(跳空),liquidate() 同样面临滑点,但执行时机由策略完全控制。

监控强平风险的实用方法

实盘中最怕的就是收到交易所的强平通知。虽然 Jesse 的实盘插件会处理,但最好自己监控。一个实用方法是计算保证金率:

def update_position(self):
    # 计算当前保证金率
    if self.position.value > 0:
        margin_ratio = self.position.margin / self.position.value
        
        # 如果保证金率低于 1%,预警
        if margin_ratio < 0.01:
            self.log(f'保证金率过低: {margin_ratio:.4f}')
            
            # 主动平一半仓位
            self.stop_loss = self.position.qty / 2, self.price

self.position.margin 是仓位占用的保证金,self.position.value 是仓位市值。保证金率越低,强平风险越高。设置预警线,在接近危险区时主动减仓,能避免被强制平仓的命运。

对于多策略组合,还可以在策略之间动态调配保证金。某个策略表现好时,从其他策略抽调资金过来,提高资金效率。这涉及到跨策略通信,可以通过共享状态或数据库实现,属于高级应用。


仓位与风险管理是量化交易的生存之本。Jesse 把这些工具都准备好了,关键是我们如何在策略里组合运用。从 risk_to_qty 的精确计算,到动态止损止盈的调整,再到对强平风险的主动监控,每一步都是在把不确定性关进笼子里。下一章会讲策略优化,但请记住,再牛的优化也救不了一个风控有缺陷的策略。先活下来,再谈赚钱。