首板客必看的反常识结论-过去一年首板及炸板数据统计

阿凡
阿凡 2025,大牛市快来了 V:flj99103681

2 人点赞了该文章 · 791 浏览

对2024年1月1日到2025年1月22日期间,所有首板封住及首板炸板的标的进行数据统计,分析首次涨停时间与封板率及当天打板在第二天开盘收益的关系,结论还是有些反常识的:仅从上板时间段来做选择,每天下午2点到3点的板,回报率是最高的。
次日溢价率的计算公式如下:
次日溢价率=(次日开盘价-当日涨停价)/当日收盘价 //
说明:主要是考虑到买入价都是涨停价

具体情况大家可以自己看,我先上数据和初步结论,大家可以自己琢磨一下,最后老规矩,文后贴源码。

1、样本情况

查询到的记录数: 25019
其中涨停数: 16043
炸板数: 8976
总体炸板率: 35.88%

2、按
首次涨停按时间分布(小时)的打首板数据

时间段样本数炸板数涨停数炸板率次日平均溢价溢价标准差成功率(溢价>0)
14:00-15:0073642593477135.21%0.91%5.80%50.60%
11:00-12:001815610120533.61%-0.21%5.34%45.29%
10:00-11:0048041649315534.33%-0.24%5.49%45.71%
9:30-10:00110364124691237.37%-0.75%6.65%44.28%
按小时看,最优交易时间表格(按平均溢价排序):
时间段平均溢价样本数炸板率风险指标(标准差)成功率
14:00-15:000.91%736435.21%5.80%50.60%
11:00-12:00-0.21%181533.61%5.34%45.29%
10:00-11:00-0.24%480434.33%5.49%45.71%
9:30-10:00-0.75%1103637.37%6.65%44.28%
下面是可视化展示:

3、尾盘打板收益更高的确反直觉(大佬们都让别去下午的板),为了进一步了解数据分布,按15分钟的维度,再做了一遍统计

时间段样本数炸板数涨停数炸板率次日平均溢价溢价标准差成功率(溢价>0)
09:30-09:4585963344525238.90%-0.87%6.90%43.66%
09:45-10:002440780166031.97%-0.34%5.67%46.48%
10:00-10:151601561104035.04%-0.58%5.28%43.35%
10:15-10:30123843480435.06%-0.29%5.65%45.56%
10:30-10:4599834165734.17%-0.28%5.54%45.69%
10:45-11:0096731365432.37%0.42%5.53%49.84%
11:00-11:1584627557132.51%-0.28%5.30%44.92%
11:15-11:3088231057235.15%-0.19%5.34%45.58%
11:30-11:4587256228.74%0.30%5.74%45.98%
13:00-13:151886609127732.29%1.59%5.71%58.80%
13:15-13:3091833658236.60%-0.28%4.48%44.34%
13:30-13:4588231356935.49%0.84%5.55%48.87%
13:45-14:0078529349237.32%1.27%6.02%51.59%
14:00-14:1578732845941.68%2.57%6.61%59.34%
14:15-14:3066323942436.05%0.03%5.24%45.85%
14:30-14:4568123644534.65%-0.50%4.72%40.53%
14:45-15:0075423951531.70%0.71%7.11%42.84%
15:00-15:158080.00%1.70%7.99%50.00%

成功率和回报率最高的居然是下午2点!!

根据上面的统计,得到的最佳打首板时间如下:


时间段平均溢价样本数炸板率风险指标(标准差)成功率
14:00-14:152.57%78741.68%6.61%59.34%
13:00-13:151.59%188632.29%5.71%58.80%
15:00-15:151.70%80.00%7.99%50.00%
13:45-14:001.27%78537.32%6.02%51.59%
13:30-13:450.84%88235.49%5.55%48.87%
10:45-11:000.42%96732.37%5.53%49.84%
11:30-11:450.30%8728.74%5.74%45.98%
14:45-15:000.71%75431.70%7.11%42.84%
14:15-14:300.03%66336.05%5.24%45.85%
09:45-10:00-0.34%244031.97%5.67%46.48%
10:15-10:30-0.29%123835.06%5.65%45.56%
10:30-10:45-0.28%99834.17%5.54%45.69%
11:00-11:15-0.28%84632.51%5.30%44.92%
13:15-13:30-0.28%91836.60%4.48%44.34%
11:15-11:30-0.19%88235.15%5.34%45.58%
10:00-10:15-0.58%160135.04%5.28%43.35%
14:30-14:45-0.50%68134.65%4.72%40.53%
09:30-09:45-0.87%859638.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-01-23 20:41

免责声明:

本文由 阿凡 原创发布于 百果量化交流平台 ,著作权归作者所有。

登录一下,更多精彩内容等你发现,贡献精彩回答,参与评论互动

登录! 还没有账号?去注册

kydrill
2025-02-26 17:23
膜拜,太棒了
benihz
2025-02-14 17:34
跟我的认知完全不一样啊