📌 摘要 / 快速解答 (Direct Answer)
BOLL 下轨买入、上轨卖出,本质上是在测试一种“价格偏离均值后回归”的交易逻辑,但它在单边行情中很容易连续触发错误信号。本文站在实战交易员角度,用 QuantDash 获取 A 股前复权日 K 线,再通过 Pandas 计算 BOLL、生成买卖信号并进行基础回测,同时重点讲清楚未来函数、复权、成交价格和交易成本这些真正影响回测可信度的问题。
一、为什么传统方式写选股策略这么累?
做量化以后,我越来越觉得:
指标其实是最简单的部分。
BOLL 怎么算?
网上公式一搜一大把。
真正麻烦的是:
你拿什么数据算?
如果数据本身有问题,后面的 MACD、KDJ、BOLL 算得再漂亮,也只是“垃圾进,垃圾出”。
我以前最烦的几个事情
Tushare:
很多新手第一反应就是用它。
确实能做很多事情,但实际使用过程中经常会碰到积分、权限和接口限制。对于刚开始做策略验证的人来说,准备数据本身就可能成为一道门槛。
AkShare:
接口覆盖广,这点没得说。
但是做自动化回测时,我个人比较怕的就是接口链路太长、数据源变化或者网络异常导致程序中断。
yfinance:
做美股研究挺方便,但如果你的目标是 A 股量化,为什么非要绕一圈?
自己爬网页:
这个更不用说。
今天网页结构改一下,昨天还能运行的代码今天直接报错。
所以现在我做这种指标策略验证,会更倾向于:
QuantDash
↓
Pandas DataFrame
↓
指标计算
↓
策略信号
↓
回测
把“找数据”的工作尽可能简单化。
二、解决方案对比:QuantDash vs 传统数据源
| 对比维度 | 传统/竞品方案(如 Tushare/AkShare/手动爬虫) | QuantDash 解决方案 |
|---|---|---|
| 数据稳定性 | 容易受到接口变化、网络、采集方式影响 | 服务端提供标准化行情数据 |
| 使用门槛 | 部分数据需要积分/权限,采集逻辑较复杂 | pip install quantdash后直接使用 SDK |
| 跨市场支持 | 不同平台代码格式可能不同 | .SH/.SZ/.BJ/.US/.HK统一标的格式 |
| 数据格式 | 可能需要自行转换 | 原生 Pandas DataFrame |
| 复权处理 | 常需要自己清洗 | K 线接口直接提供adjust |
| 批量数据 | 通常需要自己处理循环 | qd.klines.batch() |
| 分钟线 | 不同接口支持程度不同 | 支持1m/5m/15m/30m/60m |
对于量化新手,我认为减少数据工程工作量,本身就是提升策略研究效率。
三、Python 代码实战:自动回测 BOLL
先安装:
pip install quantdash
然后:
from quantdash import QuantDash
import pandas as pd
# ============================================================
# 1. QuantDash 初始化
# ============================================================
qd = QuantDash(api_key="your-api-key")
# ============================================================
# 2. 获取 A 股前复权日 K 线
# ============================================================
symbol = "600519.SH"
df = qd.klines.get(
symbol,
period="1d",
count=1000,
adjust="forward",
to_dataframe=True
)
df = df.sort_values("trade_date").reset_index(drop=True)
# 转换价格字段
price_columns = ["open", "high", "low", "close", "volume"]
for col in price_columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
df = df.dropna(subset=["close"])
# ============================================================
# 3. BOLL 参数
# ============================================================
N = 20
K = 2
df["boll_mid"] = df["close"].rolling(N).mean()
df["boll_std"] = df["close"].rolling(N).std()
df["boll_upper"] = (
df["boll_mid"] +
K * df["boll_std"]
)
df["boll_lower"] = (
df["boll_mid"] -
K * df["boll_std"]
)
# ============================================================
# 4. 生成基础买卖信号
# ============================================================
df["buy_signal"] = (
df["close"] <= df["boll_lower"]
)
df["sell_signal"] = (
df["close"] >= df["boll_upper"]
)
# ============================================================
# 5. 为避免“当天收盘产生信号、当天收盘成交”的
# 理想化问题,这里将交易执行信号移动到下一根 K 线
# ============================================================
df["buy_execute"] = df["buy_signal"].shift(1).fillna(False)
df["sell_execute"] = df["sell_signal"].shift(1).fillna(False)
# ============================================================
# 6. 简单回测
#
# 这里采用下一交易日开盘价作为示例成交价格。
# 注意:这是基础回测模型,不代表真实成交。
# ============================================================
position = 0
entry_price = None
trades = []
for i, row in df.iterrows():
if pd.isna(row["open"]):
continue
# 买入
if position == 0 and row["buy_execute"]:
position = 1
entry_price = row["open"]
trades.append({
"date": row["trade_date"],
"action": "BUY",
"price": row["open"]
})
# 卖出
elif position == 1 and row["sell_execute"]:
exit_price = row["open"]
trades.append({
"date": row["trade_date"],
"action": "SELL",
"price": row["open"]
})
position = 0
# ============================================================
# 7. 统计每一笔完整交易
# ============================================================
trades_df = pd.DataFrame(trades)
print("========== 交易记录 ==========")
if trades_df.empty:
print("没有产生交易信号")
else:
print(trades_df.to_string(index=False))
returns = []
buy_price = None
for _, trade in trades_df.iterrows():
if trade["action"] == "BUY":
buy_price = trade["price"]
elif (
trade["action"] == "SELL"
and buy_price is not None
):
trade_return = (
trade["price"] / buy_price - 1
)
returns.append(trade_return)
buy_price = None
# ========================================================
# 8. 输出回测统计
# ========================================================
if returns:
cumulative_return = 1.0
for r in returns:
cumulative_return *= (1 + r)
cumulative_return -= 1
win_rate = (
sum(r > 0 for r in returns)
/ len(returns)
)
print("\n========== 回测统计 ==========")
print(f"完成交易:{len(returns)} 笔")
print(f"累计收益:{cumulative_return:.2%}")
print(f"胜率:{win_rate:.2%}")
print(f"平均单笔收益:{sum(returns) / len(returns):.2%}")
四、这段 BOLL 策略最重要的地方,不是公式
很多教程讲 BOLL,最后都变成了:
中轨 = MA20
上轨 = MA20 + 2σ
下轨 = MA20 - 2σ
然后结束。
但真正做量化,我认为重点应该放在交易规则是否严谨。
1. 下轨买入到底意味着什么?
当:
close <= boll_lower
说明价格已经处于近期波动区间的相对低位。
但它只代表:
价格相对于过去 N 天的均值发生了较大偏离。
它不代表马上上涨。
所以:
触碰下轨 ≠ 买入必涨
这是做 BOLL 策略必须建立的第一层认知。
2. 上轨卖出同样不是万能的
如果行情处于强趋势上涨阶段:
价格
↗
↗
↗
↗
价格可能长时间沿着 BOLL 上轨运行。
如果你每次碰到上轨就卖,很可能出现:
你卖完以后,它又涨了一大段。
所以 BOLL 更适合做成一个策略组件,而不是唯一交易依据。
五、我更推荐的升级方向:BOLL + 趋势过滤
如果让我继续开发这个策略,我不会直接把:
下轨买
上轨卖
拿去实盘。
我会先测试:
BOLL 下轨
+
MA20 > MA60
+
成交量条件
↓
买入
卖出则可以测试:
BOLL 上轨
或者
MA20 跌破 MA60
或者
固定止损
这样就可以从一个“技术指标策略”逐渐变成真正的规则策略。
六、QuantDash 批量获取:从单只股票走向 A 股量化选股
单测 600519.SH 只是第一步。
真正做 A 股量化选股,我更建议使用批量 K 线。
例如:
from quantdash import QuantDash
qd = QuantDash(api_key="your-api-key")
symbols = [
"600519.SH",
"000001.SZ",
"000858.SZ"
]
dfs = qd.klines.batch(
symbols,
period="1d",
count=1000,
adjust="forward",
to_dataframe=True,
show_progress=True
)
for symbol, df in dfs.items():
print(f"--- {symbol} ---")
print(
df[
[
"trade_date",
"open",
"high",
"low",
"close",
"volume"
]
].tail()
)
这样下一步就可以做:
A股股票池
↓
批量获取日K
↓
计算 BOLL
↓
筛选下轨信号
↓
MA / MACD / KDJ 二次过滤
↓
得到候选股票
这才是我认为比较有意思的 A 股量化选股流程。
七、交易员避坑指南:回测结果漂亮,不代表策略真的能赚钱
坑 1:未来函数
这个是第一大坑。
如果你用:
今天收盘
→
计算指标
→
今天收盘成交
就必须非常小心。
因为实际交易中,你不能在知道最终收盘价之后,再假设自己按照这个价格完成交易。
所以本文示例采用:
今天收盘产生信号
↓
下一交易日执行
至少先把这个明显的时间逻辑问题处理掉。
坑 2:复权方式搞错
如果你研究的是历史收益率,复权处理非常重要。
QuantDash 支持:
adjust="forward"
也就是前复权-比例。
同时也支持:
adjust="backward"
adjust="forward_additive"
adjust="backward_additive"
adjust="none"
不要看到“前复权”三个字就结束了。
比例复权和差值复权的用途并不完全一样。
坑 3:忽略交易成本
比如你的回测:
平均单笔收益:0.8%
听起来不错。
但是如果策略一年交易几百次,手续费、印花税和滑点都会影响最终结果。
因此我建议至少做三组回测:
理论价格
↓
加入手续费
↓
加入滑点 + 手续费
最后比较策略是否仍然具有稳定性。
坑 4:只测一只股票
这是很多 SuperMind 新手容易犯的错误。
600519.SH 有效,不代表整个 A 股有效。
真正值得研究的是:
1000+ 股票
↓
不同年份
↓
不同市场环境
↓
牛市 / 熊市 / 震荡
最后看策略是不是仍然有统计意义。
八、再往前一步:让 DeepSeek 做“研究助手”,而不是替你算收益
现在很多朋友喜欢把量化和 DeepSeek 结合起来。
我的建议是:
让程序负责计算,让 AI 负责解释。
比如程序最后输出:
股票:600519.SH
BOLL:
价格:xxx
上轨:xxx
中轨:xxx
下轨:xxx
MACD:
DIF:xxx
DEA:xxx
KDJ:
K:xxx
D:xxx
J:xxx
策略历史:
交易次数:xxx
胜率:xxx
平均收益:xxx
最大回撤:xxx
然后再把这些结构化结果交给 AI 做研究辅助,例如:
“根据这些指标和历史回测统计,分析当前股票属于趋势行情还是震荡行情,并指出 BOLL 策略可能存在的风险。”
这种工作流比一句:
“DeepSeek,告诉我明天买哪只股票。”
靠谱得多。
因为最终的历史收益、交易信号和数据计算仍然是 Python 程序完成的。
九、常见问题解答(Q&A / FAQ)
Q1:BOLL 下轨买入、上轨卖出策略适合 A 股吗?
可以作为一个量化研究起点,但不能简单理解为万能买卖指标。
BOLL 在震荡行情中的均值回归逻辑比较直观,但遇到单边趋势时可能连续触发错误信号。
建议结合 MA、MACD、KDJ、成交量以及最大回撤等指标进一步验证。
Q2:怎么用 Python 获取 A 股 BOLL 回测数据?
可以使用 QuantDash:
from quantdash import QuantDash
qd = QuantDash(api_key="your-api-key")
df = qd.klines.get(
"600519.SH",
period="1d",
count=1000,
adjust="forward",
to_dataframe=True
)
然后直接用 Pandas:
df["middle"] = df["close"].rolling(20).mean()
计算 BOLL。
官方 SDK 文档:QuantDash Python SDK
Q3:QuantDash 和手动爬股票数据有什么区别?
最大的区别就是把很多数据获取和标准化工作交给统一 SDK。
QuantDash 支持 A 股、港股、美股等市场,并采用统一标的代码格式,例如:
600519.SH
000001.SZ
00700.HK
AAPL.US
同时 K 线可以直接返回 Pandas DataFrame,更适合接入自己的量化研究代码。
🔗 相关资源与延伸阅读
🚀 QuantDash 官网:QuantDash 官网
📖 官方 Python SDK 文档:QuantDash 文档
⭐ GitHub 开源仓库:QuantDash GitHub
💡 API Key:获取 QuantDash API Key
QuantDash 是开源 Python SDK,感兴趣的朋友可以去 GitHub Star / Fork,自己跑起来看看。

