作 者:老余捞鱼
原创不易,转载请标明出处及原作者。

文中示例仅用于技术讨论,不构成任何操作建议。
量化策略开发应以学习和技术交流为目的。
本号不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。
本文约 3000 字 | 预计阅读 5-8 分钟,内有实例代码,建议先收藏。
· · ·
先看一组数字。
策略总回报 2779%,同期长期持有这只股票是 1235%。10 万本金变成 287 万,147 笔交易,夏普比率 0.675,胜率 52.1%。
这是 AXON 股票从 2005 年到 2026 年的回测结果,数值看起来不错,但真正值钱的不是这条曲线,我们往下看。
① 策略到底在干什么
这套策略只有两个条件,一个管进场,一个管离场。
进场很反直觉,是在股票走弱的时候买。怎么判断走弱?用一个叫 CCI 的指标。你可以把它理解成一把尺子,量的是价格现在离它最近的平均位置有多远。当今天的 CCI 比 5 天前还低,说明价格正往偏离均值的方向往下走,动能越来越弱。就在这个”看起来要跌”的节骨眼,策略反而进场。
离场看”启明星”,一个由三根 K 线组成的小反转信号。第一根是阴线,第二根实体缩到第一根的一半以下,说明下跌的劲儿快用完了,第三根收阳,多头开始反攻。看到这个形态就离场。
在弱市进场、在反弹离场,不追高。这套逻辑上世纪 80 年代的技术分析书里就有,不新鲜,但简单意味着不容易坏。

下面这段是完整代码,一行没删,你可以直接复制去跑:
# simple_strategy.py
# 标的:AXON | 2026-06-18 08:27
import pandas as pd
import numpy as np
import yfinance as yf
import vectorbt as vbt
# -------------------------
# 下载数据
# -------------------------
symbol = "AXON"
start_date = "1960-01-01"
end_date = "2026-08-21"
interval = "1d"
df = yf.download(symbol, start=start_date, end=end_date, interval=interval, multi_level_index=False)
df.reset_index().to_csv("AXON_clean.csv", index=False)
# -------------------------
# 必要参数
# -------------------------
CCI_PERIOD = 14 # CCI 计算周期
CCI_SHIFT = 5 # CCI 比较移位步长
# -------------------------
# 指标函数
# -------------------------
def cci_falling(df, period=CCI_PERIOD, shifts=CCI_SHIFT):
"""判断 CCI 是否持续下降(当前值小于 shifts 天前的值)"""
df['CCI_main'] = calculate_cci(df, period)['CCI']
return df['CCI_main'] < df['CCI_main'].shift(shifts)
def calculate_cci(df, period=CCI_PERIOD):
"""
计算 Commodity Channel Index (CCI) —— 商品通道指数
"""
df = df.copy()
# 典型价格 (TP) = (最高价 + 最低价 + 收盘价) / 3
df['TP'] = (df['High'] + df['Low'] + df['Close']) / 3
# TP 的简单移动平均
df['SMA_TP'] = df['TP'].rolling(period).mean()
# 平均绝对偏差
df['MeanDev'] = df['TP'].rolling(period).apply(
lambda x: np.mean(np.abs(x - np.mean(x))), raw=True
)
# CCI 计算公式:(TP - SMA_TP) / (0.015 * MeanDev)
df['CCI'] = (df['TP'] - df['SMA_TP']) / (0.015 * df['MeanDev'])
return df
def morning_star(df):
"""
早晨之星(Morning Star)形态识别:
c1: 前两日收盘价 < 前两日开盘价(阴线)
c2: 前一日实体长度 < 前两日实体长度的 50%(十字星或小实体)
c3: 当日收盘价 > 当日开盘价(阳线)
"""
c1 = df['Close'].shift(2) < df['Open'].shift(2)
c2 = abs(df['Close'].shift(1) - df['Open'].shift(1)) < \
abs(df['Close'].shift(2) - df['Open'].shift(2)) * 0.5
c3 = df['Close'] > df['Open']
return c1 & c2 & c3
# -------------------------
# 入场条件
# -------------------------
df["CCI_Falling"] = cci_falling(df) # CCI 下降
# -------------------------
# 出场条件
# -------------------------
df["Morning_Star"] = morning_star(df) # 出现早晨之星
# -------------------------
# 信号合成
# -------------------------
entry_conditions = [
'CCI_Falling', # 所有入场条件(可扩展)
]
exit_conditions = [
'Morning_Star', # 所有出场条件(可扩展)
]
df['entry_signal'] = df[entry_conditions].all(axis=1) # 入场信号(同时满足所有入场条件)
df['exit_signal'] = df[exit_conditions].all(axis=1) # 出场信号(同时满足所有出场条件)
# -------------------------
# 回测引擎
# -------------------------
# 将信号向后偏移一天,避免使用未来数据(用昨日信号决定今日操作)
shift_entries = df['entry_signal'].shift(1).astype(bool).fillna(False).to_numpy()
shift_exits = df['exit_signal'].shift(1).astype(bool).fillna(False).to_numpy()
pf = vbt.Portfolio.from_signals(
close=df['Open'], # 以开盘价成交
entries=shift_entries,
exits=shift_exits,
init_cash=100_000, # 初始资金 10 万美元
fees=0.001, # 手续费率 0.1%
slippage=0.002, # 滑点 0.2%
freq='1d' # 日频交易
)
# -------------------------
# 输出绩效统计 / 绘图
# -------------------------
print(pf.stats())参数值yfinance负责抓行情,vectorbt负责算回测。信号延迟一根K线,也就是今天算出信号,第二天开盘才成交,防止”早知道”作弊。手续费 0.1%,滑点 0.2%,这两个成本不加进去,回测数字会好看得离谱。
② 为什么不能只看单次回测
单次回测有一个致命问题,它有可能偷看了答案。
拿着 2005 到 2026 的完整数据,不断调参数调到收益最好。这回答的是”如果提前知道答案,能赚多少”。但真实交易里,你永远不知道明天会怎样。
我用的办法叫”前推验证”,英文叫 Walk-forward。用过去 4 年的数据定参数,第 5 年完全不看结果、盲跑一年,然后整体往前挪一年,再重复。这样跑出来的 2779%,每一笔交易的参数,都只来自它之前的历史数据,没有一笔是偷看过未来的。
12年的逐年成绩单长这样:
| 年份 | CCI周期 | 偏移 | 收益 | 夏普 | 最大回撤 |
|---|---|---|---|---|---|
| 2005 | 11 | 4 | -74.22% | -1.287 | -78.01% |
| 2006 | 11 | 4 | +31.67% | 0.995 | -22.38% |
| 2007 | 11 | 4 | +70.98% | 1.508 | -32.11% |
| 2008 | 11 | 4 | -34.98% | -0.229 | -64.78% |
| 2009 | 11 | 4 | -13.05% | -0.066 | -35.14% |
| 2012 | 17 | 5 | +70.87% | 1.856 | -23.91% |
| 2014 | 17 | 4 | +58.67% | 1.381 | -48.83% |
注意 CCI 周期这一列,在 11 到 17 之间来回变。这说明市场状态在变,参数必须跟着调,你没法一套参数吃一辈子。也说明这个策略的赚钱能力是看行情吃饭的,不是稳定的。12 年里有 5 年是亏的,赚钱的年份也得扛 25% 到 50% 的回撤。
③ 压力测试才是重点
跑赢长期持有,只是上半场。下半场是搞清楚,里面到底藏着什么。
我们把 147 笔交易当成一副牌,洗了 1000 次,每次洗牌就是一条新的资金曲线。同样的交易,不同的先后顺序,不同的运气。结果如下:
| 分位 | 总回报 | 最大回撤 | 夏普 |
|---|---|---|---|
| 5%(最差) | -52.23% | -96.73% | 0.18 |
| 50%(中位) | +2646.43% | -82.74% | 0.56 |
| 95%(最好) | +156997.42% | -66.53% | 0.93 |
得到了两个信息,一个让人安心,一个让人清醒。
安心的那个。真实回测的 2779%,正好落在分布的中位附近。说明这个结果不是靠运气排出来的,是它该有的位置。

需要我们清醒的是:哪怕运气最好的 5% 路径,也要回撤 66%,中位路径回撤 83%。也就是说,无论交易顺序怎么排,这条资金曲线都不会舒服。
但这个结果不该是大家放弃本策略的理由,而该是用来控制仓位的工具。
④ 仓位怎么定,让数字说话
回撤 79%,如果压上全部身家,谁也扛不住。但同样的回撤,如果只占总资金的 10%,那对整体也就是 7.9% 的损失,肉疼,但不致命。
把压力测试的结果套上不同的仓位,一目了然:
| 仓位分配 | 中位回撤 | 最差5%回撤 |
|---|---|---|
| 100% | -82.7% | -96.7% |
| 25% | -20.7% | -24.2% |
| 10% | -8.3% | -9.7% |
| 5% | -4.1% | -4.8% |
10% 仓位下,这个策略最坏的情况,让你亏的还没一次普通股市回调多,而它赚钱的那一面,复利照样在总账户里体现。蒙特卡洛模拟不是告诉你”要不要做这个策略”的判决书,是告诉你”该放多少钱”的尺子。
还有一件事必须老实交代。AXON 是一只股票,到2026 年时,我们都知道它已经涨了 13 倍。如果一个策略只在你早就知道答案的那只票上有效,那它就不是真本事。下一步该做的是拿一篮子股票跑,赚钱的、亏钱的都跑一遍。如果只有这只票能跑出收益,整个结论都得推翻。
⑤ 这套流程才是真正该学的
生成 → 前推验证 → 压力测试 → 定仓位。第一步只要 90 秒,后面加起来大约一小时。
这个循环才是精髓。策略可以换,CCI 换成 RSI,AXON 换成沪深 300 的 ETF,vectorbt 换成 backtrader,都行。但流程一步不能省。
回测告诉你”能不能做”,压力测试告诉你”放多少钱”。两个问题,两个答案,缺一不可。
#量化策略 #蒙特卡罗 #仓位管理 #压力测试 #CCI指标 #Walk-Forward #回测 #风险控制 #AXON #策略评估 #量化交易 #交易系统
Be First to Comment