从筹码分布到获利比率:Python实战模拟通达信winner函数
1. 理解筹码分布与获利比率的核心逻辑
我第一次接触winner函数时,也被这个看似简单却内涵丰富的指标吸引住了。简单来说,winner函数计算的是在当前收盘价下,市场上有多少比例的持仓是盈利的。这个指标之所以有价值,是因为它反映了市场参与者的整体盈亏状态,这对判断市场情绪和潜在买卖压力非常有帮助。
要理解winner函数,首先得搞清楚筹码分布这个概念。筹码分布描述的是市场上持仓成本的价格分布情况。想象一下,市场上每个投资者买入股票的价格都不一样,有的在10元买的,有的在12元买的。筹码分布就是统计这些不同买入价对应的持仓量。
由于我们无法获取每个投资者的真实持仓数据,只能通过交易数据来估算。这里的关键假设是:每日的换手率会影响筹码的重新分布。比如某只股票第一天全部筹码都在10元,第二天有20%的换手率,平均交易价格是11元,那么第二天的筹码分布就会变成80%在10元,20%在11元。
2. 筹码分布的迭代计算模型
理解了基本概念后,我们来看具体的计算模型。筹码分布的计算是一个迭代过程,需要逐日更新。让我们用一个更详细的例子来说明:
假设某只股票总流通股为1000万股:
- 第1天:平均交易价格10元,换手率0%
- 第2天:换手率20%,平均交易价格11元
- 第3天:换手率30%,平均交易价格12元
计算过程如下:
- 第1天结束时:所有1000万股的成本都是10元
- 第2天:
- 保留的筹码:1000万 × (1-20%) = 800万股(仍为10元成本)
- 新交易的筹码:1000万 × 20% = 200万股(11元成本)
- 第3天:
- 从10元保留的筹码:800万 × (1-30%) = 560万股
- 从11元保留的筹码:200万 × (1-30%) = 140万股
- 新交易的筹码:1000万 × 30% = 300万股(12元成本)
这个模型的核心在于:每日的换手部分会以当天的平均价格形成新的筹码,而未换手部分则保留原有成本。通过这种迭代方式,我们可以构建出整个市场的持仓成本分布。
3. Python实现winner函数的关键步骤
现在我们来用Python实现这个计算过程。我将代码分成几个关键部分讲解:
3.1 数据准备
首先需要获取必要的历史数据:
def winner_core(ContextInfo, close): close_price = close[-1] # 获取最新收盘价 # 获取过去250天的成交量和成交金额数据 df = ContextInfo.get_market_data(['volume', 'amount'], stock_code=ContextInfo.get_universe(), skip_paused=True, period='1d', end_time=close.index[-1], count=250) df = df.loc[df['volume'] != 0] # 过滤掉停牌日 df['mean'] = df['amount'] / df['volume'] / 100 # 计算日均价这里我们获取了成交量(volume)和成交金额(amount),并计算出每日的平均交易价格(mean)。
3.2 换手率处理
换手率数据是关键输入,需要特别注意处理:
# 获取换手率数据 turnover_rate = ContextInfo.get_turnover_rate(ContextInfo.get_universe(), df.index[0], df.index[-1]) df['turnover'] = turnover_rate['000001.SZ'].values df['turnover'][0] = 0 # 首日换手率设为0 # 计算保留比例 df['1_turnover'] = 1 - df['turnover'] df['2_turnover'] = df['1_turnover'][::-1].values df['3_turnover'] = df['2_turnover'].shift(periods=1) df['3_turnover'][0] = 1这段代码做了几件事:
- 获取历史换手率数据
- 计算每日的保留比例(1 - 换手率)
- 对保留比例进行时间序列处理,为后续的累积计算做准备
3.3 筹码分布计算
这是最核心的部分,实现了我们前面讨论的迭代模型:
# 计算累积保留比例 df['4_turnover'] = df['3_turnover'].cumprod()[::-1].values df['turnover'][0] = 1 # 计算各价格区间的筹码分布 df['chouma'] = df['turnover'] * df['4_turnover'] # 计算获利比率 return df.loc[df['mean'] < close_price]['chouma'].sum()4_turnover计算的是从当日到计算日的累积保留比例,而chouma则是各价格区间的筹码分布。最后,我们将所有低于当前收盘价的筹码相加,就得到了获利比率。
4. 完整函数实现与封装
为了让代码更易用,我们可以将上述逻辑封装成完整的函数:
def winner(ContextInfo, close): result = [] n = len(close) for i in range(n): res = winner_core(ContextInfo, close[:i+1]) result.append(res) return pd.Series(result, index=close.index)这个封装函数可以处理整个时间序列,为每个交易日计算对应的获利比率。在实际使用中,你可以这样调用:
# 假设context是回测上下文,close是收盘价序列 profit_ratio = winner(context, close)5. 验证与调优
实现完算法后,验证其准确性很重要。我们可以通过与同花顺等专业软件的数据对比来验证:
在我的测试中,对于平安银行(000001)这样的流动性较好的股票,我们的计算结果与同花顺的数据非常接近,误差通常在1%以内。但对于交易不活跃的小盘股,差异可能会大一些,这主要是因为:
- 我们的模型假设每日换手均匀,但实际上大单交易可能集中在特定价格
- 平均价格的计算方法不同
- 停牌和复牌日的处理方式可能有差异
如果要提高准确性,可以考虑以下优化方向:
- 使用Tick级数据:如果能获取更精细的交易数据,可以更准确地计算筹码分布
- 考虑大单交易:对大单交易赋予不同权重
- 调整换手率计算窗口:根据股票特性优化换手率的计算周期
6. 实际应用场景
winner函数计算出的获利比率在实际分析中非常有用,以下是几个典型应用场景:
- 支撑压力分析:当获利比率接近100%时,可能面临获利回吐压力;当接近0%时,可能形成支撑
- 市场情绪判断:高获利比率可能伴随乐观情绪,但也可能意味着回调风险
- 策略信号生成:可以结合其他指标构建交易信号,如获利比率突破某阈值时触发交易
例如,你可以创建一个简单的策略:
def initialize(context): # 策略初始化 pass def handle_data(context, data): # 计算获利比率 profit_ratio = winner(context, data.history('close', 250, '1d')) current_ratio = profit_ratio[-1] # 简单策略:当获利比例低于30%时买入,高于70%时卖出 if current_ratio < 0.3: order_target_percent(context.stock, 1.0) elif current_ratio > 0.7: order_target_percent(context.stock, 0.0)7. 常见问题与解决方案
在实际使用中,你可能会遇到以下问题:
数据不足问题:对于新股,历史数据不足会导致计算结果不准确。解决方案是设置最小计算窗口,如至少需要60个交易日数据。
停牌处理:股票停牌期间没有交易数据。我们的代码已经通过
skip_paused=True参数跳过了停牌日,但要注意复牌首日的换手率可能会异常高。极端行情下的失真:在连续涨停或跌停时,由于交易不充分,平均价格可能无法反映真实成本。这时可以考虑使用限价订单簿数据来改进。
计算效率优化:对于全市场计算,原始实现可能较慢。可以考虑以下优化:
# 使用numpy向量化计算 def vectorized_winner(df, close_prices): # 向量化实现代码 pass参数调优:默认使用250天历史数据,但对于不同特性的股票,可以调整这个参数。高波动性股票可能需要更短的窗口。
8. 进阶思考与扩展
掌握了基础实现后,你可以进一步探索以下方向:
多时间框架分析:同时计算日线、周线、月线级别的获利比率,获取更全面的市场视角。
结合成交量分析:给不同成交量水平下的筹码赋予不同权重,比如放量突破时的筹码可能更重要。
动态换手率调整:根据市场波动性动态调整换手率计算窗口,在波动大时使用更短窗口。
板块与市场整体分析:计算整个板块或市场的获利比率,用于判断整体市场情绪。
机器学习增强:使用机器学习模型来学习专业软件中的winner函数,进一步提高准确性。
实现这些扩展需要更复杂的代码,但核心思想仍然是基于筹码分布的基本原理。比如,动态换手率调整可以这样实现:
def dynamic_window_winner(context, close, volatility_lookback=30): # 计算历史波动率 returns = np.log(close/close.shift(1)) volatility = returns.rolling(volatility_lookback).std() # 根据波动率动态调整窗口 windows = np.where(volatility > 0.02, 60, 250) # 高波动用短窗口 result = [] for i in range(len(close)): window = windows[i] if np.isnan(window) or i < window: result.append(np.nan) continue res = winner_core(context, close[i-window:i+1]) result.append(res) return pd.Series(result, index=close.index)这个实现根据股票的历史波动率动态调整计算窗口,在波动大时使用60天窗口,平时使用250天窗口。
