134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
import pandas as pd
|
||
import numpy as np
|
||
from sqlalchemy import create_engine, text
|
||
|
||
|
||
def init_db(engine):
|
||
"""
|
||
初始化数据库表结构
|
||
"""
|
||
create_table_sql = """
|
||
CREATE TABLE IF NOT EXISTS `stock_limit_data` (
|
||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
`trade_date` DATE NOT NULL COMMENT '交易日期',
|
||
`symbol` VARCHAR(10) NOT NULL COMMENT '股票代码',
|
||
`ts_code` VARCHAR(20) NOT NULL COMMENT 'TS股票代码',
|
||
`name` VARCHAR(50) NOT NULL COMMENT '股票名称',
|
||
`status` TINYINT NOT NULL COMMENT '状态:1-涨停,2-炸板',
|
||
`limit_price` DECIMAL(16, 4) COMMENT '计算出的理论涨停价',
|
||
`close` DECIMAL(16, 4) COMMENT '实际收盘价',
|
||
`market` VARCHAR(20) COMMENT '所属板块',
|
||
UNIQUE KEY `uniq_date_code` (`trade_date`, `ts_code`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
"""
|
||
with engine.begin() as conn:
|
||
conn.execute(text(create_table_sql))
|
||
|
||
|
||
def analyze_and_save_limits(start_date, end_date):
|
||
"""
|
||
计算并在数据库中落地涨停、炸板详情
|
||
"""
|
||
# 数据库连接配置
|
||
engine = create_engine(
|
||
"mysql+pymysql://yangfan@8.159.129.156:10836/level?charset=utf8mb4",
|
||
connect_args={
|
||
"password": "aA%8023321088"
|
||
},
|
||
pool_pre_ping=True
|
||
)
|
||
|
||
init_db(engine)
|
||
|
||
# 1. 获取行情数据
|
||
query = f"""
|
||
SELECT
|
||
d.trade_date, d.symbol, d.ts_code, b.name,
|
||
d.high, d.close, d.pre_close, b.market
|
||
FROM stock_daily d
|
||
JOIN stock_basic b ON d.ts_code = b.ts_code
|
||
WHERE d.trade_date >= '{start_date}'
|
||
AND d.trade_date <= '{end_date}'
|
||
AND d.pre_close IS NOT NULL
|
||
ORDER BY d.trade_date ASC
|
||
"""
|
||
|
||
try:
|
||
df = pd.read_sql(query, engine)
|
||
if df.empty:
|
||
print(f"⚠️ 区间 [{start_date} , {end_date}] 内未获取到数据。")
|
||
return
|
||
|
||
# 强制类型转换
|
||
df['pre_close'] = pd.to_numeric(df['pre_close'], errors='coerce')
|
||
df['high'] = pd.to_numeric(df['high'], errors='coerce')
|
||
df['close'] = pd.to_numeric(df['close'], errors='coerce')
|
||
|
||
# 2. 涨停价计算规则 (已取消 ST 判定)
|
||
def calc_limit(row):
|
||
m = str(row['market'])
|
||
p = row['pre_close']
|
||
|
||
if pd.isna(p) or p <= 0: return 0.0
|
||
|
||
# 仅保留板块判定
|
||
if '创业板' in m or '科创板' in m:
|
||
ratio = 1.2
|
||
elif '北交所' in m:
|
||
ratio = 1.3
|
||
else:
|
||
ratio = 1.1 # 普通主板(含ST股现在也按10%计算)
|
||
|
||
# 采用标准四舍五入
|
||
return round(p * ratio, 2)
|
||
|
||
df['limit_price'] = df.apply(calc_limit, axis=1)
|
||
|
||
# 3. 筛选逻辑:利用 abs 差值判断,解决浮点数精度问题
|
||
# 只要最高价触及理论涨停价(误差0.01以内)即视为触板
|
||
is_at_limit = (df['high'] - df['limit_price']).abs() < 0.011
|
||
is_close_at_limit = (df['close'] - df['limit_price']).abs() < 0.011
|
||
|
||
df['is_limit'] = is_at_limit & is_close_at_limit
|
||
df['is_broken'] = is_at_limit & (~is_close_at_limit)
|
||
|
||
# 分离结果
|
||
limit_df = df[df['is_limit']].copy()
|
||
limit_df['status'] = 1
|
||
|
||
broken_df = df[df['is_broken']].copy()
|
||
broken_df['status'] = 2
|
||
|
||
save_df = pd.concat([limit_df, broken_df])
|
||
|
||
if not save_df.empty:
|
||
final_df = save_df[[
|
||
'trade_date', 'symbol', 'ts_code', 'name',
|
||
'status', 'limit_price', 'close', 'market'
|
||
]]
|
||
|
||
# 4. 事务入库:先删后插
|
||
with engine.begin() as conn:
|
||
target_dates = final_df['trade_date'].astype(str).unique()
|
||
date_list = ", ".join([f"'{d}'" for d in target_dates])
|
||
|
||
delete_query = f"DELETE FROM stock_limit_data WHERE trade_date IN ({date_list})"
|
||
conn.execute(text(delete_query))
|
||
|
||
final_df.to_sql('stock_limit_data', conn, if_exists='append', index=False)
|
||
|
||
print(f"✅ 处理成功!涉及交易日: {len(target_dates)} 天")
|
||
print(f"📊 记录详情: 总计 {len(final_df)} 条 (涨停: {len(limit_df)} / 炸板: {len(broken_df)})")
|
||
else:
|
||
print("ℹ️ 该时段内全市场无涨停或炸板记录。")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 运行失败: {e}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 配置日期区间
|
||
START_DATE = '2024-09-23'
|
||
END_DATE = '2026-04-27'
|
||
|
||
analyze_and_save_limits(START_DATE, END_DATE) |