1. 为什么K线之外还需要筹码分布
很多人做股票分析,打开软件第一眼就是K线图,红红绿绿看涨跌,均线、MACD、KDJ叠一堆指标,结果还是经常被套。问题出在哪?K线只告诉你价格在什么位置成交过,但没告诉你谁在什么价位拿着多少筹码。同样是一根阳线,底部放量拉升和顶部对倒出货,K线形态可能差不多,但背后的筹码结构天差地别。
筹码分布图解决的就是这个问题。它把历史上每个价位的成交量累积起来,形成一条横向的“持仓成本分布”,让你一眼看出当前价格上方有多少套牢盘、下方有多少获利盘。而Winner函数(获利盘比例函数)则是把这张图量化成一个0到1之间的数字,直接告诉你当前价位下,市场上有多少比例的筹码是赚钱的。
这两个东西结合起来,判断支撑和压力的逻辑就非常清晰了:获利盘比例极低的位置,往往意味着大量筹码被套,抛压沉重,是压力位;获利盘比例极高但价格回调到某个区间时,如果筹码峰没有明显上移,说明主力没走,那个位置就是强支撑。
我用Python把这套逻辑完整实现了一遍,从数据获取到筹码分布计算,再到Winner函数的可视化,全部自己写。下面把整个思路、代码和踩过的坑都摊开讲。
2. 核心原理拆解:筹码分布到底怎么算
2.1 从成交量到筹码分布的数学逻辑
筹码分布的本质是一个价格-成交量累积模型。假设某只股票第一天在10元成交了1000手,第二天在10.5元成交了800手,那么10元位置就有1000手的筹码,10.5元有800手。但现实没这么简单,因为筹码会“流动”——今天在10元买入的人,明天可能在10.5元卖出,那10元的筹码就减少了,10.5元的筹码增加了。
所以标准的筹码分布算法需要引入一个衰减因子,模拟筹码的换手过程。常见的有两种模型:
- 三角形分布模型:假设当天的成交量在最高价和最低价之间呈三角形分布,峰值在均价附近。这是通达信等软件常用的方法。
- 均匀分布模型:假设当天的成交量在最高价和最低价之间均匀分布。计算简单,但精度略低。
我选择的是三角形分布模型,因为它更贴近真实成交情况。具体做法是:对每一天,取最高价、最低价、均价三个点,构建一个三角形,然后把这个三角形的面积(即当天成交量)分配到各个价位上。
衰减的处理方式是:每天先对所有历史筹码乘以一个衰减系数,再把当天的新筹码加进去。衰减系数通常用换手率来近似,公式是:
衰减系数 = 1 - 换手率换手率越高,历史筹码被替换得越快,衰减越厉害。这个逻辑很直观:如果一只股票每天换手10%,那10天之后原来的筹码基本就换了一遍。
2.2 Winner函数的定义与计算
Winner函数(获利盘比例)的定义是:当前价格以下的筹码总量占总筹码的比例。用公式表示:
Winner(price) = 价格price以下的筹码累积量 / 总筹码量如果Winner(10) = 0.85,意思是当前价格10元时,85%的筹码是获利的,只有15%被套。这个数字越高,说明获利盘越多,抛压可能越大;数字越低,说明套牢盘越多,上方压力越重。
但注意,Winner函数不是简单的“价格以下成交量占比”,因为筹码分布是动态变化的。你必须先用上面的衰减模型算出当前时刻的筹码分布,再对价格做累积求和。这也是为什么自己写代码比直接看软件指标更灵活——你可以调整衰减参数,观察不同换手率假设下的筹码结构变化。
2.3 支撑压力的判断逻辑
有了筹码分布和Winner函数,判断支撑压力就有了量化依据:
- 筹码峰位置:筹码分布图上最高的那个峰,代表最多人持仓的成本区。价格跌到这个位置,容易获得支撑;涨到这个位置,容易遇到压力。
- Winner极低区:如果当前价格对应的Winner值低于0.1,说明90%以上的人被套,这个位置上方就是密集套牢区,反弹到那里大概率被压回来。
- Winner突变点:Winner函数曲线斜率突然变缓的位置,往往对应筹码密集区,是关键的支撑压力转换点。
我实际用下来,最有效的信号是“价格回调但Winner不降”。什么意思?价格从高位跌下来,但Winner函数没有同步大幅下降,说明低位筹码峰没动,主力还在里面,这个回调就是洗盘,后面大概率还有一波。
3. 环境准备与数据获取
3.1 Python环境配置
我用的环境是Python 3.10 + VS Code,这是目前最顺手的组合。如果你还没装Python,直接去官网下载安装包,安装时勾选“Add Python to PATH”,这一步很关键,不然后面命令行调用会出问题。
需要的库不多,核心就几个:
pip install pandas numpy matplotlib akshare- pandas:数据处理,筹码分布的计算全靠它。
- numpy:数值计算,三角形分布的插值用得上。
- matplotlib:画图,筹码分布图和Winner曲线都靠它。
- akshare:免费的数据接口,获取A股历史行情很方便。
注意:akshare的接口偶尔会变动,如果某个函数报错,先去官方文档查最新用法。我踩过这个坑,旧教程里的函数名可能已经废弃了。
如果你习惯用PyCharm,配置逻辑一样,只是界面不同。VS Code的好处是轻量,配合Jupyter插件可以边写边看结果,调试筹码分布这种计算密集型代码特别方便。
3.2 获取历史行情数据
数据是筹码分布的原料,质量直接决定结果准不准。我用akshare获取日线数据,核心字段包括:日期、开盘、最高、最低、收盘、成交量、换手率。
import akshare as ak import pandas as pd def get_stock_data(code, start_date, end_date): df = ak.stock_zh_a_hist( symbol=code, period="daily", start_date=start_date, end_date=end_date, adjust="qfq" # 前复权,保证价格连续性 ) df = df.rename(columns={ "日期": "date", "开盘": "open", "最高": "high", "最低": "low", "收盘": "close", "成交量": "volume", "换手率": "turnover" }) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) return df这里有几个关键点:
- 必须用前复权数据。不复权的数据在除权除息日会出现价格跳空,筹码分布会完全乱掉。前复权把历史价格调整到当前价格体系下,保证连续性。
- 换手率字段很重要。如果数据源没有换手率,可以用成交量除以流通股本估算,但精度会差一些。
- 数据长度至少覆盖一年。太短的话筹码分布还没稳定,参考价值有限。我一般取两年以上的数据。
实操心得:akshare的换手率字段有时候是空值,尤其是早期数据。遇到这种情况,我直接用成交量除以流通股本手动算。流通股本可以从akshare的股票基本信息接口获取。
4. 筹码分布计算的核心实现
4.1 三角形分布模型的代码实现
这是整个项目最核心的部分。思路是:对每一天,根据最高价、最低价、均价构建三角形,把当天成交量分配到各个价位上,同时对所有历史筹码做衰减。
import numpy as np def calc_chip_distribution(df, price_step=0.01): """ 计算筹码分布 df: 包含date, high, low, close, volume, turnover的DataFrame price_step: 价格分辨率,默认0.01元 """ # 确定价格范围 price_min = df["low"].min() price_max = df["high"].max() prices = np.arange(price_min, price_max + price_step, price_step) n_prices = len(prices) # 初始化筹码数组 chips = np.zeros(n_prices) for i, row in df.iterrows(): high = row["high"] low = row["low"] close = row["close"] volume = row["volume"] turnover = row["turnover"] / 100 # 换手率转小数 # 衰减:历史筹码乘以(1-换手率) decay = 1 - turnover if decay < 0: decay = 0 chips = chips * decay # 当天筹码的三角形分布 # 均价用(high+low+close)/3近似 avg = (high + low + close) / 3 # 找到价格区间对应的索引 mask = (prices >= low) & (prices <= high) if not mask.any(): continue segment_prices = prices[mask] # 构建三角形权重 weights = np.zeros_like(segment_prices) for j, p in enumerate(segment_prices): if p <= avg: if avg - low > 0: weights[j] = (p - low) / (avg - low) else: if high - avg > 0: weights[j] = (high - p) / (high - avg) # 归一化权重,乘以成交量 weight_sum = weights.sum() if weight_sum > 0: weights = weights / weight_sum * volume # 累加到筹码数组 chips[mask] += weights return prices, chips这段代码有几个细节值得展开:
衰减系数的处理。我用的是1 - 换手率,但实际中换手率可能超过100%(比如新股上市首日),这时候decay会变成负数,所以我加了if decay < 0: decay = 0的保护。另外,有些股票换手率极低(比如大盘蓝筹),衰减很慢,筹码分布会保留很多历史信息,这其实是合理的——大盘股筹码本来就稳定。
三角形权重的构建。当价格等于均价时权重最大,向两边递减到0。这模拟了真实成交中“均价附近成交最密集”的规律。如果最高价等于最低价(一字板),三角形退化成一条线,代码里用if avg - low > 0和if high - avg > 0做了保护。
价格分辨率的选择。price_step=0.01元对大多数股票够用,但如果你分析的是高价股(比如几百元的),可以适当放大到0.1元,否则数组太大影响性能。我试过0.01元分析一只300元的股票,价格数组有3万多个点,计算速度明显变慢。
4.2 Winner函数的计算
有了筹码分布,Winner函数就是简单的累积求和:
def calc_winner(prices, chips, current_price): """ 计算当前价格的获利盘比例 """ total_chips = chips.sum() if total_chips == 0: return 0 # 当前价格以下的筹码 mask = prices <= current_price profit_chips = chips[mask].sum() return profit_chips / total_chips这个函数可以进一步扩展,比如计算任意价格区间的筹码占比,或者找出筹码峰的位置:
def find_chip_peaks(prices, chips, top_n=3): """ 找出筹码分布的前N个峰值 """ from scipy.signal import find_peaks peaks, properties = find_peaks(chips, height=chips.max()*0.1, distance=20) peak_prices = prices[peaks] peak_heights = chips[peaks] # 按高度排序 sorted_idx = np.argsort(peak_heights)[::-1][:top_n] return peak_prices[sorted_idx], peak_heights[sorted_idx]find_peaks的distance参数控制峰之间的最小距离,我设的是20个价格单位(即0.2元),避免把同一个峰拆成多个。height参数过滤掉太小的峰,只保留主要筹码峰。
注意:scipy不是必装库,如果你不想引入额外依赖,可以自己写一个简单的峰值检测:遍历筹码数组,如果一个点的值大于左右各N个点的值,就认为是峰。效果差不多,代码更可控。
5. 可视化实现:让筹码分布一目了然
5.1 筹码分布图的绘制
筹码分布图的标准画法是横向柱状图,价格在纵轴,筹码量在横轴。我习惯把当前价格用一条横线标出来,再把Winner值标注在图上。
import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams['font.sans-serif'] = ['SimHei'] matplotlib.rcParams['axes.unicode_minus'] = False def plot_chip_distribution(prices, chips, current_price, winner, title="筹码分布图"): fig, ax = plt.subplots(figsize=(10, 8)) # 横向柱状图 ax.barh(prices, chips, height=0.01, color='steelblue', alpha=0.7) # 当前价格线 ax.axhline(y=current_price, color='red', linestyle='--', linewidth=1.5, label=f'当前价: {current_price:.2f}') # Winner标注 ax.text(chips.max()*0.7, current_price*1.02, f'Winner: {winner:.2%}', fontsize=12, color='red') ax.set_xlabel('筹码量') ax.set_ylabel('价格') ax.set_title(title) ax.legend() plt.tight_layout() plt.show()这里有几个视觉优化的点:
- 柱状图的height参数要和price_step匹配,我设的0.01对应0.01元的价格分辨率,柱子之间不会留缝。
- 当前价格线用红色虚线,醒目但不遮挡筹码分布。
- Winner值标注在图的右上方,避免和筹码峰重叠。
实际画出来之后,你会发现筹码分布图的信息量比K线大得多。K线只能看到价格走势,筹码分布能直接看到“哪个价位堆了多少人”。
5.2 Winner函数曲线的绘制
单独看一个时点的Winner值不够,把Winner随时间的变化画出来,才能看出趋势:
def plot_winner_curve(df, prices, chips_history, title="Winner函数曲线"): """ chips_history: 每个交易日对应的筹码分布列表 """ winners = [] for i, row in df.iterrows(): w = calc_winner(prices, chips_history[i], row["close"]) winners.append(w) fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), gridspec_kw={'height_ratios': [2, 1]}) # 上方:价格走势 ax1.plot(df["date"], df["close"], color='black', linewidth=1) ax1.set_ylabel('价格') ax1.set_title(title) ax1.grid(True, alpha=0.3) # 下方:Winner曲线 ax2.plot(df["date"], winners, color='blue', linewidth=1.5) ax2.axhline(y=0.5, color='gray', linestyle='--', alpha=0.5) ax2.fill_between(df["date"], 0, winners, alpha=0.2, color='blue') ax2.set_ylabel('Winner') ax2.set_ylim(0, 1) ax2.grid(True, alpha=0.3) plt.tight_layout() plt.show()这个双图联动很有用:上面看价格,下面看Winner。当价格创新低但Winner没有创新低时,就是底背离信号,说明虽然价格跌了,但筹码结构在改善,套牢盘在减少。
实操心得:计算chips_history的时候,不要每天都重新算一遍完整历史,那样太慢。我的做法是维护一个累积的chips数组,每天只做衰减和增量更新。这样计算复杂度从O(N²)降到O(N),速度快很多。
5.3 支撑压力位的自动标注
光看图还不够,最好能自动标出关键的支撑压力位。我的做法是找筹码峰,然后把当前价格上方最近的峰标为压力,下方最近的峰标为支撑:
def find_support_resistance(prices, chips, current_price): """ 找出当前价格上下的关键筹码峰 """ from scipy.signal import find_peaks peaks, _ = find_peaks(chips, height=chips.max()*0.05, distance=30) peak_prices = prices[peaks] # 下方的峰作为支撑 support = peak_prices[peak_prices < current_price] support = support[-1] if len(support) > 0 else None # 上方的峰作为压力 resistance = peak_prices[peak_prices > current_price] resistance = resistance[0] if len(resistance) > 0 else None return support, resistance这个逻辑简单但实用。实际用的时候,我会把支撑压力位画在筹码分布图上,用绿色标支撑、红色标压力,一眼就能看出盈亏比。
6. 实战案例:用真实数据验证效果
6.1 案例选择与数据准备
我选了一只2023年波动比较大的股票做测试,数据从2022年初到2024年中,覆盖了完整的下跌、筑底、反弹周期。这种走势最能检验筹码分布的有效性。
数据获取和计算的过程前面已经写了,这里直接看结果。计算完成后,我先画了最新交易日的筹码分布图,然后画了Winner曲线。
6.2 筹码分布图的解读
最新交易日的筹码分布图显示,筹码主要集中在三个区域:
- 高位密集区:对应2023年上半年的高位震荡区间,这部分筹码已经被套了很久,换手率低,衰减慢,仍然大量存在。
- 中位密集区:对应2023年底的反弹高点,套牢盘相对较少。
- 低位密集区:对应最近几个月的筑底区间,筹码在快速累积。
当前价格刚好在低位密集区的上沿,Winner值约为0.35。这意味着65%的筹码被套,上方压力不小。但关键信号是:低位密集区的筹码峰在最近一个月明显增高,而高位密集区在缓慢衰减,说明有资金在低位吸筹。
6.3 Winner曲线的关键信号
Winner曲线在整个周期内出现了三次重要信号:
- 第一次底背离:价格在2023年10月创新低,但Winner没有创新低,反而从0.08回升到0.15。随后价格反弹了约20%。
- 高位钝化:2024年2月价格快速拉升,Winner从0.3飙升到0.85,但随后价格滞涨,Winner在0.8以上横盘。这是典型的获利盘过多、抛压积累的信号,之后价格回调了15%。
- 第二次底背离:最近价格再次回调,但Winner在0.3附近企稳,没有跌破前低。结合筹码分布看,低位筹码峰在增强,我判断这个位置有较强支撑。
这三个信号里,底背离的可靠性最高。我统计了一下,在测试周期内,底背离出现了4次,其中3次随后出现了超过10%的反弹,胜率75%。高位钝化的信号出现2次,都出现了回调,但幅度不好预测。
6.4 参数敏感性分析
筹码分布的计算结果对参数比较敏感,我做了几组对比:
| 参数 | 取值 | 效果 |
|---|---|---|
| 衰减系数 | 1-换手率 | 标准做法,筹码更新速度适中 |
| 衰减系数 | 1-换手率*0.5 | 筹码更新慢,历史筹码保留多,适合长线分析 |
| 衰减系数 | 1-换手率*1.5 | 筹码更新快,近期筹码权重大,适合短线 |
| 价格分辨率 | 0.01元 | 精度高,计算慢 |
| 价格分辨率 | 0.05元 | 精度略低,速度快3倍 |
我最终用的是标准衰减系数+0.01元分辨率。如果你做短线,可以把衰减加快,让筹码分布更反映近期情况;做长线则相反。
注意:衰减系数的调整没有绝对标准,不同股票的最佳参数可能不同。我的建议是先用标准参数跑一遍,然后根据信号质量微调。如果发现筹码峰位置和实际支撑压力位偏差大,再调整衰减速度。
7. 常见问题与排查技巧
7.1 数据类问题
问题一:换手率数据缺失或异常
akshare的换手率字段有时候是NaN,或者出现明显异常值(比如超过100%)。我的处理方式是:
# 用成交量除以流通股本估算换手率 df["turnover"] = df["volume"] / circulating_shares * 100 # 异常值处理:超过50%的换手率截断 df["turnover"] = df["turnover"].clip(upper=50)换手率超过50%的情况极少,除非是新股或者重大事件,一般可以截断处理。如果不处理,衰减系数会变成负数,筹码分布直接崩掉。
问题二:前复权数据的价格跳空
前复权虽然解决了除权除息的问题,但在某些数据源上,前复权价格可能出现负值(比如分红特别多的股票)。遇到这种情况,我改用后复权数据,或者直接用不复权数据但手动处理除权日。
问题三:数据长度不够
如果只取几个月的数据,筹码分布还没稳定,Winner函数的参考价值很低。我的经验是至少取一年,最好两年以上。数据越长,筹码分布越稳定,但计算也越慢。折中方案是取两年数据,但只对最近半年做详细分析。
7.2 计算类问题
问题一:计算速度太慢
如果每天重新计算完整历史,时间复杂度是O(N²),N=500天的话就是25万次循环,Python里可能要跑几十秒。优化方法是维护累积chips数组:
chips_history = [] chips = np.zeros(n_prices) for i, row in df.iterrows(): chips = chips * (1 - row["turnover"]/100) # 只计算当天的增量,加到chips上 daily_chips = calc_daily_chips(row, prices) chips += daily_chips chips_history.append(chips.copy())这样复杂度降到O(N),速度快了一个数量级。
问题二:筹码总量不守恒
理论上,衰减后的历史筹码加上当天新筹码,总量应该等于历史总成交量。但实际计算中,由于价格分辨率有限,会有少量筹码“丢失”。我的处理方式是每天归一化一次:
chips = chips / chips.sum() * target_totaltarget_total可以设为最近N天的总成交量,或者直接用1(归一化)。归一化之后,Winner函数的计算不受影响,因为它是比例。
问题三:峰值检测不准
find_peaks的参数需要根据价格分辨率调整。如果price_step=0.01,distance=20意味着峰之间至少隔0.2元。对于低价股(比如5元),这个距离可能太大,会漏掉一些峰;对于高价股(比如100元),又太小,会把一个峰拆成多个。我的做法是根据当前价格动态调整:
distance = max(10, int(current_price * 0.02 / price_step))这样峰之间的距离大约是当前价格的2%,比较合理。
7.3 解读类问题
问题一:Winner值高但价格不跌
Winner值高说明获利盘多,但不一定马上跌。如果筹码峰在低位密集且没有上移,说明主力还在,高Winner可能是主力拉升的结果。这时候要看筹码分布,不能只看Winner数字。
问题二:筹码峰和实际支撑压力不符
筹码峰是历史成交密集区,但实际支撑压力还受市场情绪、基本面等因素影响。我的经验是:筹码峰作为参考,结合K线形态和成交量综合判断。如果价格在筹码峰位置放量反弹,支撑有效;如果缩量跌破,支撑失效。
问题三:不同时间周期的筹码分布不一致
日线、周线、月线算出来的筹码分布肯定不一样。我的做法是以日线为主,周线为辅。日线看短期筹码变化,周线看中长期筹码结构。如果两者信号一致,可靠性更高。
7.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 筹码分布图空白 | 数据为空或价格范围错误 | 检查数据获取和price_min/max |
| Winner值始终为0或1 | 筹码数组未正确累积 | 检查衰减和增量更新逻辑 |
| 计算速度极慢 | 每天重算完整历史 | 改用累积数组更新 |
| 筹码峰位置偏移 | 衰减系数不合适 | 调整换手率系数 |
| 图形显示中文乱码 | matplotlib字体问题 | 设置SimHei字体 |
| 换手率异常 | 数据源问题 | 手动计算或截断处理 |
8. 进阶玩法:把筹码分布接入实时监控
基础版本是离线分析,但实际交易中需要实时更新。我的做法是用定时任务每天收盘后更新数据,重新计算筹码分布,然后把关键信号推送到手机。
具体实现是用schedule库做定时:
import schedule import time def daily_update(): df = get_stock_data("000001", "20220101", "20241231") prices, chips = calc_chip_distribution(df) winner = calc_winner(prices, chips, df["close"].iloc[-1]) # 判断信号 if winner < 0.1: print("警告:Winner极低,可能超跌") elif winner > 0.9: print("警告:Winner极高,注意抛压") # 保存结果 df.to_csv("latest_data.csv") schedule.every().day.at("15:30").do(daily_update) while True: schedule.run_pending() time.sleep(60)这个框架可以进一步扩展:接入多个股票、计算筹码集中度、对比不同周期的Winner变化。我目前维护了一个包含20只股票的监控列表,每天收盘后自动更新,第二天开盘前看一遍信号。
实操心得:实时监控的关键是只计算增量。每天只需要用当天的新数据更新筹码数组,不需要重算历史。这样即使监控100只股票,计算时间也在可接受范围内。
另外,筹码分布还可以和成交量、均线等指标结合。比如:当价格在筹码峰位置缩量企稳,同时Winner从低位回升,这是比较可靠的买点信号。我回测过这个组合信号,胜率比单用筹码分布高不少。
最后说一个我踩过的坑:不要过度拟合参数。我一开始花了很多时间调衰减系数,试图让历史信号全部正确,结果发现换一只股票就失效了。后来想明白了,筹码分布是概率工具,不是精确预测。标准参数+严格纪律,比精细调参更靠谱。