开心钢琴块手游
10.97M · 2025-10-19
在量化选股、趋势策略里,“箱体”是一个常见但简单有效的结构模型。箱体意味着价格在一定区间内来回震荡,有明确的支撑和阻力,这对于捕捉突破、做区间震荡交易、止盈止损都很有帮助。
传统上,我们常靠人工目测或滑窗高低点极值来画箱体。但这方式主观性强、不易批量化。若把箱体识别流程自动化、可程序化,就能跑批量股票、回测验证、嵌入策略。
这里,我想尝试一种「聚类」的思路 —— 用 DBSCAN(密度聚类) 来辅助识别箱体。核心想法是:在价格-时间平面中,价格点集合可能在箱体区间内“密集”,而在跳跃、突破、离群价等处“稀疏”或被视为噪声。以此来识别出箱体区间边界。
接下来花姐给大家讲清楚 DBSCAN 的原理、怎么用它做箱体识别、再说实战注意事项。
为了不丢基础,这里简要回顾 DBSCAN 的基本原理。理解得透,才能在金融数据里做适配。
DBSCAN 全称是 Density-Based Spatial Clustering of Applications with Noise,直译就是“基于密度的聚类方法,可识别噪声点”。它有几个核心概念:
ε(eps):在空间中,两个点之间的最大距离阈值。如果一个点 B 在 A 的 ε 半径内,则 B 被认为是邻居。
MinPts(最小点数):在该 ε 半径内,若某个点的邻居数 ≥ MinPts,则这个点是一个 “核心点”。
直接密度可达 / 间接密度可达 等关系:
噪声点:不属于任何簇的点被标为 noise(标签常标 -1)。
DBSCAN 的优点包括:
缺点也必须注意:
在金融时间序列里,用 DBSCAN 要格外小心:数据的尺度、单位(价格 vs 时间)不同,噪声多,点密度不均等。调参、预处理非常重要。
下面介绍一种思路,你可以在这个基础上改进、调参。
下面的代码包括了计算箱体和可视化代码:
import numpy as np
import pandas as pd
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
def detect_dbscan_boxes(
df,
price_col='close',
time_col=None,
eps=0.1,
min_samples=5,
min_span=5,
min_height=0.02
):
"""
基于 DBSCAN 算法识别股票价格箱体区间
参数说明:
----------
df : pd.DataFrame
包含时间和价格的行情数据
price_col : str
价格列名(默认为 'close')
time_col : str or None
时间列名,如为 None 自动使用索引序号
eps : float
DBSCAN 邻域半径(标准化空间下)
min_samples : int
DBSCAN 最小样本点数(核心点判定阈值)
min_span : int
箱体最小时间跨度(单位:时间索引)
min_height : float
箱体最小高度(价格比例)
返回:
----------
boxes : list[dict]
每个箱体的信息(时间区间、价格上下界、高度等)
df_out : pd.DataFrame
含 cluster 标签的 DataFrame,可用于可视化或后续分析
"""
df = df.copy()
# 确定时间索引列
if time_col is None:
df['time_idx'] = np.arange(len(df))
time_col = 'time_idx'
# 构造特征矩阵并标准化
X = df[[time_col, price_col]].values.astype(float)
Xs = StandardScaler().fit_transform(X)
# 运行 DBSCAN 聚类
db = DBSCAN(eps=eps, min_samples=min_samples)
labels = db.fit_predict(Xs)
df['cluster'] = labels
# 识别箱体簇
boxes = []
for lbl in sorted(set(labels)):
if lbl == -1:
continue # 忽略噪声点
sub = df[df['cluster'] == lbl]
t_min, t_max = sub[time_col].min(), sub[time_col].max()
span = t_max - t_min
p_min, p_max = sub[price_col].min(), sub[price_col].max()
height = (p_max - p_min) / p_min
if span >= min_span and height >= min_height:
boxes.append({
'label': lbl,
't_min': t_min,
't_max': t_max,
'p_min': p_min,
'p_max': p_max,
'height': height,
'count': len(sub)
})
return boxes, df
import matplotlib.pyplot as plt
import matplotlib.patches as patches
def plot_dbscan_boxes(df, boxes, price_col='close', date_col='date',
figsize=(12,6), show_labels=True):
"""
绘制收盘价与识别出的箱体区域
参数说明:
----------
df : pd.DataFrame
含日期与价格列的数据
boxes : list[dict]
detect_dbscan_boxes() 返回的箱体信息列表
price_col : str
价格列名(默认 'close')
date_col : str
日期列名(默认 'date')
figsize : tuple
图像大小
show_labels : bool
是否在图上标注箱体编号
"""
# 设置全局字体
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Zen Hei'] # 多个备选字体
# 解决负号显示问题
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=figsize)
# 绘制价格线
ax.plot(df[date_col], df[price_col], color='black', linewidth=1.2, label='Close Price')
# 绘制每个箱体
for box in boxes:
# 提取箱体信息
t_min, t_max = int(box['t_min']), int(box['t_max'])
p_min, p_max = box['p_min'], box['p_max']
# 时间转成 x 轴坐标(日期)
x0 = df.iloc[t_min][date_col]
x1 = df.iloc[t_max][date_col]
# 用 fill_between 在时间范围内画矩形,不涉及时间加法
ax.fill_between(
[x0, x1],
p_min, p_max,
color='lightskyblue',
alpha=0.3,
edgecolor='dodgerblue'
)
if show_labels:
ax.text(x0, p_max, f"#{box['label']}",
color='blue', fontsize=8, va='bottom')
ax.set_title("DBSCAN 箱体识别结果")
ax.set_xlabel("日期")
ax.set_ylabel("价格")
ax.legend()
plt.grid(True, alpha=0.3)
plt.show()
from xtquant import xtdata
def get_hq(code,start_time,end_time):
'''
基于xtquant下载股票的历史行情
'''
xtdata.enable_hello = False
xtdata.download_history_data(stock_code=code,
period='1d',
start_time=start_time,
end_time=end_time)
history_data =xtdata.get_market_data_ex(['open','high','low','close','volume','amount','preClose'],
stock_list= [code],
period='1d',
start_time= start_time,
end_time=end_time,
dividend_type='front_ratio',
fill_data=False)
df = history_data[code]
df.index = pd.to_datetime(df.index.astype(str), format='%Y%m%d')
df['date']= df.index
return df
# 示例数据
data = get_hq(code='002617.SZ',start_time='20240101',end_time='20250901')
# 调用函数
boxes, df_with_labels = detect_dbscan_boxes(
data, price_col='close', eps=0.11, min_samples=5, min_span=10, min_height=0.02
)
# 绘制结果
plot_dbscan_boxes(df_with_labels, boxes, price_col='close', date_col='date')
箱体的绘制方法,有很多地方可以优化:
最终识别到的箱体,是一种“价格上下界 + 时间跨度”的矩形结构:
你可以把每个箱体看作“在 [t_min, t_max] 这个时间窗里,价格主要在 [p_min, p_max] 之间来回震荡”的状态。
然后在策略里,你可以定义:
在不改变原有代码的基础上,我们可以通过调节参数eps, min_samples, min_span, min_height
的取值来使得箱体绘制的更符合我们的主观判断。
1、参数作用
参数 | 作用 | 调整方向 | 影响 |
---|---|---|---|
eps | DBSCAN 聚类半径(标准化空间) | 增大 → 箱体更宽更长,容易合并小簇;减小 → 箱体更窄、碎 | 最关键参数,控制簇连通性 |
min_samples | 最小样本点数 | 增大 → 只保留稠密簇(丢掉小簇/噪声);减小 → 接受稀疏簇 | 控制簇稳定性和可靠性 |
min_span | 后处理最小时间跨度 | 增大 → 去掉短箱体;减小 → 接受短箱体 | 控制横向长度 |
min_height | 后处理最小价格高度 | 增大 → 去掉窄箱体;减小 → 接受小震荡 | 控制纵向高度 |
2、调参顺序(实战建议)
2.1 先确定 DBSCAN 的核心半径 eps
方法 1:经验法,先用当前价格数据和标准化后 eps=0.12 看箱体数量。
方法 2:k-distance 图法,选 eps 对应拐点距离。
from sklearn.neighbors import NearestNeighbors
nbrs = NearestNeighbors(n_neighbors=min_samples).fit(Xs)
distances, _ = nbrs.kneighbors(Xs)
k_dist = np.sort(distances[:, min_samples-1])
plt.plot(k_dist); plt.show()
拐点位置就是 eps 的参考值。
2.2 min_samples 影响簇稠密度
2.3 min_span 控制时间长度
2.4 min_height 控制价格区间高度
3、 实用调参策略
3.1 箱体太小(碎片多、横向短、纵向窄):
3.2 箱体太少或太大(覆盖全局、失去分辨率):
3.3 调试技巧:
4、 可视化辅助
调参时强烈建议每次都画图,观察箱体:
boxes, df_with_labels = detect_dbscan_boxes(
data, price_col='close', eps=0.12, min_samples=5, min_span=10, min_height=0.02
)
plot_dbscan_boxes(df_with_labels, boxes)
通过图能直观判断:
5、 推荐调参顺序(实战简化版)
1️⃣ 先调整 eps → 控制簇连通性
2️⃣ 调整 min_samples → 控制簇稠密度
3️⃣ 调整 min_span → 控制横向长度
4️⃣ 调整 min_height → 控制纵向幅度
5️⃣ 可选:price_weight、平滑窗口、合并临近箱体
根据上面的方法,我把参数调整了下看起来效果好多了
之前的参数
boxes, df_with_labels = detect_dbscan_boxes(
data, price_col='close', eps=0.11, min_samples=5, min_span=10, min_height=0.02
)
现在的参数
boxes, df_with_labels = detect_dbscan_boxes(
data, price_col='close', eps=0.12, min_samples=5, min_span=18, min_height=0.04
)
下面讲讲在实际研究/回测/实盘里对这套方法的观察和实战建议
突破策略检测 当价格在箱体上界突破,可以作为“多头信号”;在下界跌破,可以作为“空头信号”或止损信号。相比于单纯的高低点突破,箱体结构能给信号更多维度约束(持续时间、密度、强度)。
区间震荡策略 对于震荡偏强的品种(特别是股票、小盘股、波动性较大的股票),你可以在箱体内部做“低买高卖”策略,带有止盈止损。DBSCAN 找到的箱体在逻辑上自带“密集性”保证,理论上能减少跳动噪音造成的误交易。
箱体过滤 / 辅助选股 对于一桶候选股票,你可以先筛出“近期存在明确箱体结构”的股票作为优先研究对象,因为箱体结构往往意味着市场在这个阶段有明确的支撑-阻力区间,策略较易落地点。
多周期联动 / 套件策略 你可以在多个周期上都做箱体识别(如日线、小时线、30 分钟线),然后做多周期融合(比如小时级突破 + 日线箱体确认)来增强信号的可靠性。
假设我们用日线做箱体识别,进行一个突破策略:
通过实盘或回测,你可以评估这种策略在样本里的胜率、利润因子、回撤、持仓期等。