【Python量化实盘工程 #02】仓位与风控不能靠感觉:5 条硬红线代码化

策略有了,撮合引擎也有了,但还差最关键的一环:在信号变成订单之前,先过一遍风控。

很多个人量化系统的事故,不是因为策略失效,而是因为:

  • 满仓一只票,结果黑天鹅一来直接腰斩;
  • 没有止损,浮亏 30% 还安慰自己「会回来的」;
  • 单日亏损超过心理承受线,导致后面的交易全部变形;
  • 行情波动突然放大,仓位却没降下来。

本文把 5 条最常见的硬红线写成代码,让你的策略从「我觉得没问题」变成「系统不让我冒险」。

一、为什么风控必须代码化?

口头风控的问题在于:临场决策会被情绪绑架。

代码化风控的核心思想是:在策略产生信号之后、发单之前,先跑一遍 RiskManager.check()。如果任何一条红线被触发,就把信号拦住或减仓。风控不是建议,是门禁。

二、本文你将得到什么

  1. 一个可复用的 RiskManager 类;
  2. 5 条硬红线的代码实现:
    • 最大持仓市值上限
    • 单笔止损
    • 单笔止盈
    • 单日亏损上限
    • 波动率降仓
  3. 用历史行情逐日跑风控信号的完整示例;
  4. 可直接运行的 Python 代码。

三、5 条硬红线设计

红线 触发条件 典型动作
最大持仓 单票持仓市值 > 账户上限比例 禁止再加仓
单笔止损 浮亏 ≤ -5% 强制平仓
单笔止盈 浮盈 ≥ +10% 获利了结或移动止盈
单日亏损 当日账户回撤 ≤ -3% 当日停止开新仓
波动率降仓 20 日年化波动率 > 25% 按比例降低仓位

这些阈值不是唯一标准,你可以根据自己的资金规模和策略波动率调整。关键是:先把检查写进代码,再讨论阈值。

四、完整代码实战

import math
from dataclasses import dataclass
from typing import List, Tuple
from mairui import Client

DEMO_LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073"

INITIAL_CASH = 100_000.0
MAX_POSITION_VALUE = 60_000.0
STOP_LOSS_PCT = -0.05
TAKE_PROFIT_PCT = 0.10
DAILY_LOSS_LIMIT_PCT = -0.03
VOLATILITY_LOOKBACK = 20
VOLATILITY_REDUCTION_THRESHOLD = 0.25

def annualized_volatility(returns: List[float]) -> float:
if len(returns) < 2:
return 0.0
mean = sum(returns) / len(returns)
variance = sum((r - mean) ** 2 for r in returns) / (len(returns) - 1)
return math.sqrt(variance * 252)

class RiskManager:
def check(
self,
date: str,
cash: float,
position_qty: int,
avg_cost: float,
market_price: float,
account_value: float,
daily_return: float,
) -> List[Tuple[str, str]]:
signals = []
position_value = position_qty * market_price

红线1:最大持仓

if position_value > MAX_POSITION_VALUE:
signals.append(("MAX_POSITION",
f"持仓市值 {position_value:.2f} 元超过上限 {MAX_POSITION_VALUE:.2f} 元"))

红线2/3:止损止盈

if avg_cost > 0 and position_qty > 0:
unrealized_pct = (market_price - avg_cost) / avg_cost
if unrealized_pct <= STOP_LOSS_PCT:
signals.append(("STOP_LOSS",
f"浮亏 {unrealized_pct100:.2f}% 触及止损线 {STOP_LOSS_PCT100:.2f}%"))
elif unrealized_pct >= TAKE_PROFIT_PCT:
signals.append(("TAKE_PROFIT",
f"浮盈 {unrealized_pct100:.2f}% 触及止盈线 {TAKE_PROFIT_PCT100:.2f}%"))

红线4:单日亏损上限

if daily_return <= DAILY_LOSS_LIMIT_PCT:
signals.append(("DAILY_LOSS_LIMIT",
f"当日账户回撤 {daily_return100:.2f}% 触及上限 {DAILY_LOSS_LIMIT_PCT100:.2f}%"))

return signals

def volatility_signal(self, vol: float):
if vol > VOLATILITY_REDUCTION_THRESHOLD:
return ("VOLATILITY_REDUCTION",
f"年化波动率 {vol100:.2f}% 超过阈值 {VOLATILITY_REDUCTION_THRESHOLD100:.2f}%,建议降仓")
return None

def fetch_bars(code: str):
client = Client(DEMO_LICENCE)
return client.stock_history(code=code, period="d", dividend="f")

def main():
bars = fetch_bars("600519")
risk_mgr = RiskManager()
cash = INITIAL_CASH
position_qty = 0
avg_cost = 0.0
account_value_prev = INITIAL_CASH

初始建仓

first_price = float(bars[0]["c"])
target_qty = int(MAX_POSITION_VALUE / first_price / 100) * 100
cost = target_qty * first_price
cash -= cost
position_qty = target_qty
avg_cost = first_price
print(f"初始建仓:{bars[0]['t']} 买入 {target_qty} 股 @ {first_price:.2f}\n")

for i, bar in enumerate(bars[1:], start=1):
date = bar["t"]
price = float(bar["c"])
account_value = cash + position_qty * price
daily_return = (account_value - account_value_prev) / account_value_prev

recent_returns = []
if i >= VOLATILITY_LOOKBACK:
closes = [float(b["c"]) for b in bars[i - VOLATILITY_LOOKBACK:i + 1]]
recent_returns = [closes[j] / closes[j - 1] - 1 for j in range(1, len(closes))]
vol = annualized_volatility(recent_returns)

signals = risk_mgr.check(date, cash, position_qty, avg_cost,
price, account_value, daily_return)
vol_signal = risk_mgr.volatility_signal(vol)
if vol_signal:
signals.append(vol_signal)

if signals:
print(f"{date} 收盘={price:.2f} 总值={account_value:.2f} 当日={daily_return100:.2f}% 波动={vol100:.2f}%")
for name, desc in signals:
print(f" [触发] {name}: {desc}")

account_value_prev = account_value

if name == "main":
main()

五、运行结果(演示环境)

初始建仓:2025-09-15 买入 5300 股 @ 11.14

2025-09-16 收盘=11.54 总值=102120.00 当日=2.12% 波动=0.00%
[触发] MAX_POSITION: 持仓市值 61162.00 元超过上限 60000.00 元
2025-09-19 收盘=10.43 总值=96237.00 当日=-1.78% 波动=0.00%
[触发] STOP_LOSS: 浮亏 -6.37% 触及止损线 -5.00%
2025-10-24 收盘=10.96 总值=99046.00 当日=-3.61% 波动=47.42%
[触发] DAILY_LOSS_LIMIT: 当日账户回撤 -3.61% 触及上限 -3.00%
[触发] VOLATILITY_REDUCTION: 年化波动率 47.42% 超过阈值 25.00%,建议降仓
2025-11-05 收盘=11.63 总值=102597.00 当日=3.97% 波动=55.37%
[触发] MAX_POSITION: 持仓市值 61639.00 元超过上限 60000.00 元
[触发] VOLATILITY_REDUCTION: 年化波动率 55.37% 超过阈值 25.00%,建议降仓

汇总:共触发风控信号 34 次
最终账户总值:99258.00 元,总收益 -0.74%

六、常见坑与注意

  1. 阈值不要照搬:止损 -5% / 止盈 10% 是示例。真实策略要根据历史回测的最大回撤和波动率来设定,最好先做压力测试。
  2. 单日亏损上限要锁定:一旦触发,建议当天停止开新仓,而不是只提示。否则很容易「再赌一把」。
  3. 波动率要用滚动窗口:不要用全历史波动率,否则最近的风险变化会被早期数据稀释。
  4. 风控信号 ≠ 自动平仓:本文示例只输出信号,真实系统要把它接到下单模块。建议用事件总线(Event Bus)解耦策略、风控、执行三层。
  5. 持仓均价要算对:多次加减仓时,avg_cost 应该用加权平均;如果做 T0,还要区分可卖数量和总数量。

七、小结 + 下篇预告

本文把 5 条最常见的风控红线写成了可执行的 Python 代码。核心结论是:

风控不是交易结束后的复盘,而是交易开始前的门禁。

下篇我们将在撮合引擎和风控之上,搭建一个模拟盘(Paper Trading)——用真实行情、虚拟资金、完整订单落库,把回测和实盘之间的鸿沟再缩小一步。

免责声明:本文仅供技术学习与交流,不构成任何投资建议。市场有风险,决策需谨慎。


代码与文档:https://github.com/MaiRuiApi

posted @ 2026-10-12 06:41  量化分析码农  阅读(7)  评论(0)    收藏  举报