首板客必看的反常识结论-过去一年首板及炸板数据统计
对2024年1月1日到2025年1月22日期间,所有首板封住及首板炸板的标的进行数据统计,分析首次涨停时间与封板率及当天打板在第二天开盘收益的关系,结论还是有些反常识的:仅从上板时间段来做选择,每天下午2点到3点的板,回报率是最高的。
次日溢价率的计算公式如下:
次日溢价率=(次日开盘价-当日涨停价)/当日收盘价 //说明:主要是考虑到买入价都是涨停价
具体情况大家可以自己看,我先上数据和初步结论,大家可以自己琢磨一下,最后老规矩,文后贴源码。
1、样本情况
查询到的记录数: 25019
其中涨停数: 16043
炸板数: 8976
总体炸板率: 35.88%
2、按首次涨停按时间分布(小时)的打首板数据
时间段 | 样本数 | 炸板数 | 涨停数 | 炸板率 | 次日平均溢价 | 溢价标准差 | 成功率(溢价>0) |
---|---|---|---|---|---|---|---|
14:00-15:00 | 7364 | 2593 | 4771 | 35.21% | 0.91% | 5.80% | 50.60% |
11:00-12:00 | 1815 | 610 | 1205 | 33.61% | -0.21% | 5.34% | 45.29% |
10:00-11:00 | 4804 | 1649 | 3155 | 34.33% | -0.24% | 5.49% | 45.71% |
9:30-10:00 | 11036 | 4124 | 6912 | 37.37% | -0.75% | 6.65% | 44.28% |
时间段 | 平均溢价 | 样本数 | 炸板率 | 风险指标(标准差) | 成功率 |
---|---|---|---|---|---|
14:00-15:00 | 0.91% | 7364 | 35.21% | 5.80% | 50.60% |
11:00-12:00 | -0.21% | 1815 | 33.61% | 5.34% | 45.29% |
10:00-11:00 | -0.24% | 4804 | 34.33% | 5.49% | 45.71% |
9:30-10:00 | -0.75% | 11036 | 37.37% | 6.65% | 44.28% |


3、尾盘打板收益更高的确反直觉(大佬们都让别去下午的板),为了进一步了解数据分布,按15分钟的维度,再做了一遍统计
时间段 | 样本数 | 炸板数 | 涨停数 | 炸板率 | 次日平均溢价 | 溢价标准差 | 成功率(溢价>0) |
---|---|---|---|---|---|---|---|
09:30-09:45 | 8596 | 3344 | 5252 | 38.90% | -0.87% | 6.90% | 43.66% |
09:45-10:00 | 2440 | 780 | 1660 | 31.97% | -0.34% | 5.67% | 46.48% |
10:00-10:15 | 1601 | 561 | 1040 | 35.04% | -0.58% | 5.28% | 43.35% |
10:15-10:30 | 1238 | 434 | 804 | 35.06% | -0.29% | 5.65% | 45.56% |
10:30-10:45 | 998 | 341 | 657 | 34.17% | -0.28% | 5.54% | 45.69% |
10:45-11:00 | 967 | 313 | 654 | 32.37% | 0.42% | 5.53% | 49.84% |
11:00-11:15 | 846 | 275 | 571 | 32.51% | -0.28% | 5.30% | 44.92% |
11:15-11:30 | 882 | 310 | 572 | 35.15% | -0.19% | 5.34% | 45.58% |
11:30-11:45 | 87 | 25 | 62 | 28.74% | 0.30% | 5.74% | 45.98% |
13:00-13:15 | 1886 | 609 | 1277 | 32.29% | 1.59% | 5.71% | 58.80% |
13:15-13:30 | 918 | 336 | 582 | 36.60% | -0.28% | 4.48% | 44.34% |
13:30-13:45 | 882 | 313 | 569 | 35.49% | 0.84% | 5.55% | 48.87% |
13:45-14:00 | 785 | 293 | 492 | 37.32% | 1.27% | 6.02% | 51.59% |
14:00-14:15 | 787 | 328 | 459 | 41.68% | 2.57% | 6.61% | 59.34% |
14:15-14:30 | 663 | 239 | 424 | 36.05% | 0.03% | 5.24% | 45.85% |
14:30-14:45 | 681 | 236 | 445 | 34.65% | -0.50% | 4.72% | 40.53% |
14:45-15:00 | 754 | 239 | 515 | 31.70% | 0.71% | 7.11% | 42.84% |
15:00-15:15 | 8 | 0 | 8 | 0.00% | 1.70% | 7.99% | 50.00% |
成功率和回报率最高的居然是下午2点!!
根据上面的统计,得到的最佳打首板时间如下:
时间段 | 平均溢价 | 样本数 | 炸板率 | 风险指标(标准差) | 成功率 |
---|---|---|---|---|---|
14:00-14:15 | 2.57% | 787 | 41.68% | 6.61% | 59.34% |
13:00-13:15 | 1.59% | 1886 | 32.29% | 5.71% | 58.80% |
15:00-15:15 | 1.70% | 8 | 0.00% | 7.99% | 50.00% |
13:45-14:00 | 1.27% | 785 | 37.32% | 6.02% | 51.59% |
13:30-13:45 | 0.84% | 882 | 35.49% | 5.55% | 48.87% |
10:45-11:00 | 0.42% | 967 | 32.37% | 5.53% | 49.84% |
11:30-11:45 | 0.30% | 87 | 28.74% | 5.74% | 45.98% |
14:45-15:00 | 0.71% | 754 | 31.70% | 7.11% | 42.84% |
14:15-14:30 | 0.03% | 663 | 36.05% | 5.24% | 45.85% |
09:45-10:00 | -0.34% | 2440 | 31.97% | 5.67% | 46.48% |
10:15-10:30 | -0.29% | 1238 | 35.06% | 5.65% | 45.56% |
10:30-10:45 | -0.28% | 998 | 34.17% | 5.54% | 45.69% |
11:00-11:15 | -0.28% | 846 | 32.51% | 5.30% | 44.92% |
13:15-13:30 | -0.28% | 918 | 36.60% | 4.48% | 44.34% |
11:15-11:30 | -0.19% | 882 | 35.15% | 5.34% | 45.58% |
10:00-10:15 | -0.58% | 1601 | 35.04% | 5.28% | 43.35% |
14:30-14:45 | -0.50% | 681 | 34.65% | 4.72% | 40.53% |
09:30-09:45 | -0.87% | 8596 | 38.90% | 6.90% | 43.66% |
继续放图:
最后还是分享一下源码,源码是在python3.5下运行的:
第一部分:取数据(用的是ifind的API)
import pandas as pd
def get_stock_codes(end_date):
"""
获取所有股票的代码
:param end_date: 截止日期
:return: 股票代码列表
"""
all_securities = get_all_securities(ty='stock', date=end_date)
return all_securities.index.tolist()
def process_stock_data(stock_code, start_date, end_date):
"""
处理单只股票的数据,判断首板、首板炸板情况,并记录相关信息
:param stock_code: 股票代码
:param start_date: 开始日期
:param end_date: 结束日期
:return: 处理后的股票数据,首板数据,首板炸板数据
"""
# 获取日级数据,包含 high_limit 字段
daily_price_data = get_price([stock_code], start_date, end_date, fre_step='1d',
fields=['open', 'close', 'high', 'low', 'high_limit', 'turnover'])
daily_price_df = pd.DataFrame(daily_price_data[stock_code])
# 数据整理
daily_price_df['prev_close'] = daily_price_df['close'].shift(1)
daily_price_df['prev_high_limit'] = daily_price_df['high_limit'].shift(1)
# 判断是否为首板
daily_price_df['is_first_limit_up'] = (daily_price_df['close'] >= daily_price_df['high_limit']) & \
(daily_price_df['prev_close'] < daily_price_df['prev_high_limit'])
# 判断是否为首板炸板
daily_price_df['is_first_break_limit'] = (daily_price_df['high'] == daily_price_df['high_limit']) & \
(daily_price_df['close'] < daily_price_df['high_limit']) & \
(daily_price_df['prev_high_limit'] > daily_price_df['prev_close'])
daily_price_df['Date'] = daily_price_df.index
daily_price_df['stock_code'] = stock_code
# 筛选出首板和首板炸板的日期
first_limit_up_dates = daily_price_df[daily_price_df['is_first_limit_up']].index.tolist()
first_break_limit_dates = daily_price_df[daily_price_df['is_first_break_limit']].index.tolist()
first_limit_up_data = daily_price_df.loc[first_limit_up_dates]
first_break_limit_data = daily_price_df.loc[first_break_limit_dates]
# 新增:记录首次涨停时间的列
daily_price_df['first_reach_limit_time'] = None
# 遍历首板和首板炸板日期,获取首次涨停时间
for date in set(first_limit_up_dates + first_break_limit_dates):
# 将日期转换为字符串格式
date_str = date.strftime('%Y%m%d')
print("正在计算",stock_code,"在",date_str,"首次涨停时间")
# 构建符合分钟级数据格式的开始和结束时间
start_time = '{} 09:30'.format(date_str)
end_time = '{} 15:00'.format(date_str)
# 获取当日 1 分钟数据,不包含 high_limit 字段
one_min_data = get_price([stock_code], start_date=start_time, end_date=end_time, fre_step='1m', fields=['high'])
one_min_df = pd.DataFrame(one_min_data[stock_code])
# 从日级数据中获取当日的 high_limit 值
high_limit = daily_price_df.loc[date, 'high_limit']
# 找到首次达到涨停的时间
first_reach_limit_time = one_min_df[one_min_df['high'] >= high_limit].index.min()
if first_reach_limit_time is not None:
daily_price_df.loc[date, 'first_reach_limit_time'] = first_reach_limit_time
return daily_price_df, first_limit_up_data, first_break_limit_data
def main():
start_date = '20240101'
end_date = '20250122'
stock_codes = get_stock_codes(end_date)
all_stocks_data = pd.DataFrame()
first_limit_up_stocks = {}
all_first_break_limit_stocks = {}
for i, stock_code in enumerate(stock_codes, start=1):
if i % 100 == 0:
print(i)
price_df, first_limit_up_data, first_break_limit_data = process_stock_data(stock_code, start_date, end_date)
all_stocks_data = pd.concat([all_stocks_data, price_df])
if i % 500 == 0:
all_stocks_data.to_csv("all_stocks_data{}.csv".format(i))
all_stocks_data = pd.DataFrame()
print("saved all_stocks_data{}.csv".format(i))
if not first_limit_up_data.empty:
first_limit_up_stocks[stock_code] = first_limit_up_data
all_stocks_data.to_csv("all_stocks_data_final.csv")
if __name__ == "__main__":
main()
第二部分:插入数据库
import pandas as pd
import sqlite3
import glob
import os
# 创建一个SQLite连接
conn = sqlite3.connect('stock_data.db')
# 读取所有以 all_stocks_data 开头的CSV文件
csv_files = glob.glob('涨停时间分析\\all_stocks_data*.csv') # 获取符合特定命名模式的CSV文件
print(f"找到以下文件:{csv_files}")
# 创建一个空的DataFrame来存储所有数据
all_data = pd.DataFrame()
# 遍历所有CSV文件并合并
for file in csv_files:
print(f"正在处理文件:{file}")
df = pd.read_csv(file)
all_data = pd.concat([all_data, df], ignore_index=True)
# 只将日期类型的列转换为datetime格式,其他列保持原样
date_columns = ['Date', 'first_reach_limit_time']
for col in date_columns:
if col in all_data.columns:
all_data[col] = pd.to_datetime(all_data[col])
print(f"数据列包括: {all_data.columns.tolist()}")
# 将所有列的数据写入SQLite数据库
all_data.to_sql('stock_data', conn, if_exists='replace', index=False)
# 创建索引以提高查询性能
cursor = conn.cursor()
cursor.execute('CREATE INDEX IF NOT EXISTS idx_date ON stock_data(Date)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_stock_code ON stock_data(stock_code)')
# 关闭连接
conn.close()
print(f"成功导入 {len(all_data)} 条数据到SQLite数据库")
第三部分:数据分析
import pandas as pd
import sqlite3
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
# 设置中文字体和图表样式
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
plt.style.use('seaborn')
# 连接数据库
conn = sqlite3.connect('stock_data.db')
# 查询数据
query = """
WITH base_data AS (
SELECT
a.*,
b.open as next_open
FROM stock_data a
LEFT JOIN stock_data b ON a.stock_code = b.stock_code
AND b.Date = (
SELECT MIN(Date)
FROM stock_data c
WHERE c.stock_code = a.stock_code
AND c.Date > a.Date
)
WHERE (a.is_first_break_limit = 1 OR a.is_first_limit_up = 1)
AND a.first_reach_limit_time IS NOT NULL
)
SELECT
first_reach_limit_time,
is_first_break_limit,
is_first_limit_up,
high_limit,
close,
next_open,
stock_code,
Date
FROM base_data
"""
df = pd.read_sql_query(query, conn)
conn.close()
# 过滤掉没有次日开盘价的数据
df = df.dropna(subset=['next_open'])
# 计算次日溢价率(相对于涨停价)
df['next_day_premium'] = (df['next_open'] - df['high_limit']) / df['close'] * 100
# 时间处理
df['first_reach_limit_time'] = pd.to_datetime(df['first_reach_limit_time'])
df['time_only'] = df['first_reach_limit_time'].dt.time
# 将时间转换为分钟数(相对于9:30)
def time_to_minutes(t):
return t.hour * 60 + t.minute - (9 * 60 + 30)
df['minutes_from_open'] = df['time_only'].apply(time_to_minutes)
# 创建15分钟时间段标签
def get_time_period_15min(minutes):
if minutes < 0:
return '9:30之前'
base_time = datetime.strptime('09:30', '%H:%M')
current_time = base_time + pd.Timedelta(minutes=minutes)
# 处理午休时间
if current_time.hour == 12:
return '11:45-13:00'
# 创建时间段标签
period_start = current_time - pd.Timedelta(minutes=current_time.minute % 15)
period_end = period_start + pd.Timedelta(minutes=15)
return f"{period_start.strftime('%H:%M')}-{period_end.strftime('%H:%M')}"
df['time_period'] = df['minutes_from_open'].apply(get_time_period_15min)
# 统计计算
period_analysis = df.groupby('time_period').agg({
'is_first_break_limit': ['sum', 'count'],
'is_first_limit_up': 'sum',
'next_day_premium': ['mean', 'std', 'count']
})
# 将多重索引列名转换为单层索引
period_analysis.columns = [f"{col[0]}_{col[1]}" if isinstance(col, tuple) else col for col in period_analysis.columns]
# 计算各项指标
period_analysis['break_rate'] = (period_analysis['is_first_break_limit_sum'] /
period_analysis['is_first_break_limit_count'] * 100).round(2)
period_analysis['success_rate'] = df.groupby('time_period')['next_day_premium'].apply(
lambda x: (x > 0).mean() * 100).round(2)
# 创建图形
plt.figure(figsize=(20, 15))
# 1. 时间段溢价率箱线图
plt.subplot(2, 2, 1)
sns.boxplot(data=df, x='time_period', y='next_day_premium')
plt.title('Next Day Premium by 15-min Period', fontsize=12)
plt.xlabel('Time Period', fontsize=10)
plt.ylabel('Premium Rate(%)', fontsize=10)
plt.xticks(rotation=45)
# 2. 各时间段成交量和炸板率
ax2 = plt.subplot(2, 2, 2)
bars = plt.bar(period_analysis.index, period_analysis['is_first_break_limit_count'])
plt.plot(period_analysis.index, period_analysis['break_rate'], color='red', marker='o')
plt.title('Volume and Break Rate by Period', fontsize=12)
plt.xlabel('Time Period', fontsize=10)
plt.ylabel('Count', fontsize=10)
ax2.set_xticklabels(period_analysis.index, rotation=45)
ax2_twin = ax2.twinx()
ax2_twin.set_ylabel('Break Rate(%)', color='red')
plt.grid(True)
# 3. 成功率和平均溢价率
plt.subplot(2, 2, 3)
width = 0.35
x = range(len(period_analysis.index))
plt.bar([i - width/2 for i in x], period_analysis['success_rate'], width, label='Success Rate')
plt.bar([i + width/2 for i in x], period_analysis['next_day_premium_mean'], width, label='Avg Premium')
plt.title('Success Rate and Avg Premium by Period', fontsize=12)
plt.xlabel('Time Period', fontsize=10)
plt.ylabel('Rate(%)', fontsize=10)
plt.xticks(x, period_analysis.index, rotation=45)
plt.legend()
# 4. 风险收益散点图
plt.subplot(2, 2, 4)
plt.scatter(period_analysis['next_day_premium_std'],
period_analysis['next_day_premium_mean'],
s=period_analysis['is_first_break_limit_count']/30)
for i, period in enumerate(period_analysis.index):
plt.annotate(period,
(period_analysis['next_day_premium_std'][i],
period_analysis['next_day_premium_mean'][i]))
plt.xlabel('Risk (Std Dev)', fontsize=10)
plt.ylabel('Return (Avg Premium)', fontsize=10)
plt.title('Risk-Return Analysis by Period', fontsize=12)
plt.grid(True)
plt.tight_layout()
plt.savefig('trading_analysis_15min.png', dpi=300, bbox_inches='tight')
# 输出统计信息
print("\n=== 交易策略分析(15分钟间隔)===")
print(f"\n总样本数: {len(df)}")
print(f"涨停数: {df['is_first_limit_up'].sum()}")
print(f"炸板数: {df['is_first_break_limit'].sum()}")
print(f"总体炸板率: {(df['is_first_break_limit'].sum() / len(df) * 100):.2f}%")
print("\n1. 时间段详细分析:")
for period in period_analysis.index:
print(f"\n{period}:")
print(f"样本数: {period_analysis.loc[period, 'is_first_break_limit_count']:.0f}")
print(f"炸板数: {period_analysis.loc[period, 'is_first_break_limit_sum']:.0f}")
print(f"涨停数: {period_analysis.loc[period, 'is_first_limit_up_sum']:.0f}")
print(f"炸板率: {period_analysis.loc[period, 'break_rate']:.2f}%")
print(f"次日平均溢价: {period_analysis.loc[period, 'next_day_premium_mean']:.2f}%")
print(f"溢价标准差: {period_analysis.loc[period, 'next_day_premium_std']:.2f}%")
print(f"成功率(溢价>0): {period_analysis.loc[period, 'success_rate']:.2f}%")
2025-02-26 17:23
2025-02-14 17:34