Files
python/limit_up.py
T
2026-05-05 20:00:05 +08:00

747 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pandas as pd
from sqlalchemy import text
import argparse
import tushare as ts
from datetime import date
from collections import deque
import time
from stock_config import TUSHARE_TOKEN, get_engine
# --- 数据库配置 ---
# 请根据实际情况修改数据库连接信息
# DB_CONN_STR = 'mysql+pymysql://root:password@127.0.0.1:3306/stock_data'
LOCAL_START_DATE = date(2018, 1, 1)
TUSHARE_LIMIT_LIST_START_DATE = date(2020, 1, 1)
TUSHARE_LIMIT_LIST_MAX_CALLS_PER_MINUTE = 200
COMBINED_TABLE = "stock_limit_combined_data"
class MinuteRateLimiter:
def __init__(self, max_calls):
self.max_calls = max_calls
self.call_times = deque()
def wait(self):
now = time.monotonic()
while self.call_times and now - self.call_times[0] >= 60:
self.call_times.popleft()
if len(self.call_times) >= self.max_calls:
sleep_seconds = 60 - (now - self.call_times[0]) + 0.1
print(f"⏳ Tushare 限流等待 {sleep_seconds:.1f} 秒,避免超过每分钟 {self.max_calls} 次。")
time.sleep(sleep_seconds)
now = time.monotonic()
while self.call_times and now - self.call_times[0] >= 60:
self.call_times.popleft()
self.call_times.append(time.monotonic())
def normalize_trade_dates(trade_dates):
return sorted({pd.to_datetime(trade_date).date() for trade_date in trade_dates})
def filter_dates_by_start(trade_dates, start_date, source_name):
normalized_dates = normalize_trade_dates(trade_dates)
filtered_dates = [trade_date for trade_date in normalized_dates if trade_date >= start_date]
skipped_count = len(normalized_dates) - len(filtered_dates)
if skipped_count:
print(f"ℹ️ {source_name} 跳过 {skipped_count} 个早于 {start_date} 的交易日。")
return filtered_dates
def init_db(engine):
"""
初始化数据库表结构
"""
with engine.begin() as conn:
conn.execute(text(get_combined_table_sql()))
optimize_combined_table(conn)
def get_combined_table_sql():
return f"""
CREATE TABLE IF NOT EXISTS `{COMBINED_TABLE}` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`trade_date` DATE NOT NULL COMMENT '交易日期',
`symbol` VARCHAR(10) NOT NULL COMMENT '股票代码,以stock_daily为准',
`ts_code` VARCHAR(20) NOT NULL COMMENT 'TS股票代码,以stock_daily为准',
`name` VARCHAR(50) NOT NULL COMMENT '股票名称,以stock_daily关联基础表为准',
`final_status` TINYINT NOT NULL COMMENT '融合状态:1-涨停,2-炸板',
`source_count` INT NOT NULL DEFAULT 0 COMMENT '命中的数据源数量',
`source_flags` VARCHAR(50) COMMENT '命中的数据源:calc,tushare',
`calc_status` TINYINT COMMENT '本地行情计算状态:1-涨停,2-炸板',
`calc_limit_price` DECIMAL(16, 4) COMMENT '本地行情计算涨停价',
`calc_high` DECIMAL(16, 4) COMMENT 'stock_daily最高价',
`calc_close` DECIMAL(16, 4) COMMENT 'stock_daily收盘价',
`calc_pre_close` DECIMAL(16, 4) COMMENT 'stock_daily前收盘价',
`calc_vol` BIGINT COMMENT 'stock_daily成交量',
`market` VARCHAR(20) COMMENT '所属板块',
`tushare_limit_type` VARCHAR(1) COMMENT 'Tushare-D跌停U涨停Z炸板',
`tushare_industry` VARCHAR(100) COMMENT 'Tushare-所属行业',
`tushare_name` VARCHAR(50) COMMENT 'Tushare-名称',
`tushare_close` DECIMAL(16, 4) COMMENT 'Tushare-收盘价',
`tushare_pct_chg` DECIMAL(10, 4) COMMENT 'Tushare-涨跌幅',
`tushare_amount` DECIMAL(24, 4) COMMENT 'Tushare-成交额',
`tushare_limit_amount` DECIMAL(24, 4) COMMENT 'Tushare-板上成交金额',
`tushare_float_mv` DECIMAL(24, 4) COMMENT 'Tushare-流通市值',
`tushare_total_mv` DECIMAL(24, 4) COMMENT 'Tushare-总市值',
`tushare_turnover_ratio` DECIMAL(10, 4) COMMENT 'Tushare-换手率',
`tushare_fd_amount` DECIMAL(24, 4) COMMENT 'Tushare-封单金额',
`tushare_first_time` VARCHAR(20) COMMENT 'Tushare-首次封板时间',
`tushare_last_time` VARCHAR(20) COMMENT 'Tushare-最后封板时间',
`tushare_open_times` INT COMMENT 'Tushare-炸板/开板次数',
`tushare_up_stat` VARCHAR(50) COMMENT 'Tushare-涨停统计',
`tushare_limit_times` INT COMMENT 'Tushare-连板数',
UNIQUE KEY `uniq_date_code` (`trade_date`, `ts_code`),
KEY `idx_trade_date` (`trade_date`),
KEY `idx_symbol` (`symbol`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"""
def get_table_columns(conn, table_name):
return {
row[0]
for row in conn.execute(text(f"SHOW COLUMNS FROM `{table_name}`")).fetchall()
}
def index_exists(conn, table_name, index_name):
return conn.execute(
text(
"""
SELECT 1
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = :table_name
AND index_name = :index_name
LIMIT 1
"""
),
{"table_name": table_name, "index_name": index_name},
).first() is not None
def get_index_definition(conn, table_name, index_name):
rows = conn.execute(
text(
"""
SELECT column_name, non_unique
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = :table_name
AND index_name = :index_name
ORDER BY seq_in_index
"""
),
{"table_name": table_name, "index_name": index_name},
).fetchall()
if not rows:
return None
return {
"columns": [row[0] for row in rows],
"unique": rows[0][1] == 0,
}
def index_matches(conn, table_name, index_name, columns, unique=False):
index_def = get_index_definition(conn, table_name, index_name)
if not index_def:
return False
return index_def["columns"] == columns and (not unique or index_def["unique"])
def ensure_combined_columns(conn):
existing_columns = get_table_columns(conn, COMBINED_TABLE)
columns = {
"calc_vol": "BIGINT COMMENT 'stock_daily成交量'",
}
for column_name, column_def in columns.items():
if column_name not in existing_columns:
conn.execute(
text(
f"ALTER TABLE `{COMBINED_TABLE}` "
f"ADD COLUMN `{column_name}` {column_def} AFTER `calc_pre_close`"
)
)
def sync_combined_vol_from_daily(conn):
conn.execute(
text(
f"""
UPDATE `{COMBINED_TABLE}` c
JOIN `stock_daily` d
ON c.trade_date = d.trade_date
AND c.ts_code COLLATE utf8mb4_unicode_ci = d.ts_code COLLATE utf8mb4_unicode_ci
SET c.symbol = d.symbol,
c.calc_vol = d.vol
"""
)
)
def dedupe_combined_table(conn):
conn.execute(
text(
f"""
DELETE c
FROM `{COMBINED_TABLE}` c
LEFT JOIN `stock_daily` d
ON c.trade_date = d.trade_date
AND c.ts_code COLLATE utf8mb4_unicode_ci = d.ts_code COLLATE utf8mb4_unicode_ci
WHERE d.ts_code IS NULL
"""
)
)
conn.execute(
text(
f"""
DELETE c1
FROM `{COMBINED_TABLE}` c1
JOIN `{COMBINED_TABLE}` c2
ON c1.trade_date = c2.trade_date
AND c1.ts_code COLLATE utf8mb4_unicode_ci = c2.ts_code COLLATE utf8mb4_unicode_ci
WHERE
(
COALESCE(c1.source_count, 0) * 10
+ IF(c1.calc_status IS NOT NULL, 4, 0)
+ IF(c1.tushare_limit_type IS NOT NULL, 2, 0)
+ IF(c1.calc_vol IS NOT NULL, 1, 0)
) <
(
COALESCE(c2.source_count, 0) * 10
+ IF(c2.calc_status IS NOT NULL, 4, 0)
+ IF(c2.tushare_limit_type IS NOT NULL, 2, 0)
+ IF(c2.calc_vol IS NOT NULL, 1, 0)
)
OR (
(
COALESCE(c1.source_count, 0) * 10
+ IF(c1.calc_status IS NOT NULL, 4, 0)
+ IF(c1.tushare_limit_type IS NOT NULL, 2, 0)
+ IF(c1.calc_vol IS NOT NULL, 1, 0)
) =
(
COALESCE(c2.source_count, 0) * 10
+ IF(c2.calc_status IS NOT NULL, 4, 0)
+ IF(c2.tushare_limit_type IS NOT NULL, 2, 0)
+ IF(c2.calc_vol IS NOT NULL, 1, 0)
)
AND c1.id < c2.id
)
"""
)
)
def ensure_combined_indexes(conn):
dedupe_combined_table(conn)
if index_exists(conn, COMBINED_TABLE, "uniq_date_code") and not index_matches(
conn, COMBINED_TABLE, "uniq_date_code", ["trade_date", "ts_code"], unique=True
):
conn.execute(text(f"ALTER TABLE `{COMBINED_TABLE}` DROP INDEX `uniq_date_code`"))
if not index_matches(
conn, COMBINED_TABLE, "uniq_date_code", ["trade_date", "ts_code"], unique=True
):
conn.execute(
text(
f"ALTER TABLE `{COMBINED_TABLE}` "
"ADD UNIQUE KEY `uniq_date_code` (`trade_date`, `ts_code`)"
)
)
if not index_exists(conn, COMBINED_TABLE, "idx_trade_date"):
conn.execute(
text(f"ALTER TABLE `{COMBINED_TABLE}` ADD KEY `idx_trade_date` (`trade_date`)")
)
if not index_exists(conn, COMBINED_TABLE, "idx_symbol"):
conn.execute(
text(f"ALTER TABLE `{COMBINED_TABLE}` ADD KEY `idx_symbol` (`symbol`)")
)
if not index_exists(conn, COMBINED_TABLE, "idx_trade_date_status"):
conn.execute(
text(
f"ALTER TABLE `{COMBINED_TABLE}` "
"ADD KEY `idx_trade_date_status` (`trade_date`, `final_status`)"
)
)
def optimize_combined_table(conn):
ensure_combined_columns(conn)
sync_combined_vol_from_daily(conn)
ensure_combined_indexes(conn)
validate_no_duplicate_combined_rows(conn)
def validate_no_duplicate_combined_rows(conn):
duplicate_count = conn.execute(
text(
f"""
SELECT COUNT(*)
FROM (
SELECT trade_date, ts_code
FROM `{COMBINED_TABLE}`
GROUP BY trade_date, ts_code
HAVING COUNT(*) > 1
) duplicated
"""
)
).scalar()
if duplicate_count:
raise RuntimeError(f"{COMBINED_TABLE} 存在 {duplicate_count} 组重复 trade_date+ts_code 数据")
def validate_no_duplicate_frame_rows(df, frame_name):
if df.empty:
return
missing_keys = df["trade_date"].isna() | df["ts_code"].isna()
if missing_keys.any():
examples = df.loc[missing_keys, ["trade_date", "ts_code"]].head(5).to_dict("records")
raise ValueError(f"{frame_name} 存在空 trade_date 或 ts_code,示例: {examples}")
duplicated = df.duplicated(subset=["trade_date", "ts_code"], keep=False)
if duplicated.any():
examples = df.loc[duplicated, ["trade_date", "ts_code"]].head(5).to_dict("records")
raise ValueError(f"{frame_name} 存在重复 trade_date+ts_code,示例: {examples}")
def fetch_tushare_limit_list(trade_dates):
tushare_trade_dates = filter_dates_by_start(
trade_dates,
TUSHARE_LIMIT_LIST_START_DATE,
"Tushare limit_list_d",
)
if not tushare_trade_dates:
print("ℹ️ 无需调用 Tushare limit_list_d。")
return pd.DataFrame(columns=["trade_date", "symbol", "ts_code"])
ts.set_token(TUSHARE_TOKEN)
pro = ts.pro_api()
frames = []
rate_limiter = MinuteRateLimiter(TUSHARE_LIMIT_LIST_MAX_CALLS_PER_MINUTE)
total_calls = len(tushare_trade_dates) * 2
current_call = 0
fields = (
"trade_date,ts_code,industry,name,close,pct_chg,amount,limit_amount,"
"float_mv,total_mv,turnover_ratio,fd_amount,first_time,last_time,"
"open_times,up_stat,limit_times,limit"
)
print(
f"🔎 开始获取 Tushare limit_list_d:{len(tushare_trade_dates)} 个交易日,"
f"预计 {total_calls} 次调用,限速 {TUSHARE_LIMIT_LIST_MAX_CALLS_PER_MINUTE} 次/分钟。"
)
for trade_date in tushare_trade_dates:
tushare_date = pd.to_datetime(trade_date).strftime("%Y%m%d")
for limit_type in ("U", "Z"):
current_call += 1
print(f" [{current_call}/{total_calls}] 获取 {tushare_date} limit_type={limit_type} ...")
rate_limiter.wait()
try:
ts_df = pro.limit_list_d(
trade_date=tushare_date,
limit_type=limit_type,
fields=fields,
)
except Exception as exc:
print(f"⚠️ Tushare limit_list_d 获取失败: {tushare_date} {limit_type}, 错误: {exc}")
continue
if ts_df is None or ts_df.empty:
print(f" [{current_call}/{total_calls}] {tushare_date} {limit_type} 无数据。")
continue
normalized_df = normalize_tushare_limit_list(ts_df)
if not normalized_df.empty:
print(f" [{current_call}/{total_calls}] {tushare_date} {limit_type} 获取 {len(normalized_df)} 条。")
frames.append(normalized_df)
if not frames:
print("ℹ️ Tushare limit_list_d 未返回有效数据。")
return pd.DataFrame(columns=["trade_date", "symbol", "ts_code"])
result_df = pd.concat(frames, ignore_index=True).drop_duplicates(
subset=["trade_date", "ts_code", "tushare_limit_type"],
keep="first",
)
print(f"✅ Tushare limit_list_d 获取完成:{len(result_df)} 条有效记录。")
return result_df
def normalize_tushare_limit_list(ts_df):
column_map = {
"limit": "tushare_limit_type",
"industry": "tushare_industry",
"name": "tushare_name",
"close": "tushare_close",
"pct_chg": "tushare_pct_chg",
"amount": "tushare_amount",
"limit_amount": "tushare_limit_amount",
"float_mv": "tushare_float_mv",
"total_mv": "tushare_total_mv",
"turnover_ratio": "tushare_turnover_ratio",
"fd_amount": "tushare_fd_amount",
"first_time": "tushare_first_time",
"last_time": "tushare_last_time",
"open_times": "tushare_open_times",
"up_stat": "tushare_up_stat",
"limit_times": "tushare_limit_times",
}
required_columns = {"trade_date", "ts_code", "limit"}
if not required_columns.issubset(ts_df.columns):
missing_columns = sorted(required_columns - set(ts_df.columns))
print(f"⚠️ Tushare 返回字段缺失,已跳过: {', '.join(missing_columns)}")
return pd.DataFrame(columns=["trade_date", "symbol", "ts_code"])
available_map = {
source: target for source, target in column_map.items() if source in ts_df.columns
}
normalized_df = ts_df[["trade_date", "ts_code", *available_map]].rename(
columns=available_map
).copy()
normalized_df["trade_date"] = pd.to_datetime(
normalized_df["trade_date"], format="%Y%m%d"
).dt.date
normalized_df["ts_code"] = normalized_df["ts_code"].astype(str).str.strip()
normalized_df["symbol"] = normalized_df["ts_code"].str.split(".").str[0].str.zfill(6)
for column in [
"tushare_close",
"tushare_pct_chg",
"tushare_amount",
"tushare_limit_amount",
"tushare_float_mv",
"tushare_total_mv",
"tushare_turnover_ratio",
"tushare_fd_amount",
"tushare_open_times",
"tushare_limit_times",
]:
if column in normalized_df.columns:
normalized_df[column] = pd.to_numeric(normalized_df[column], errors="coerce")
for column in [
"tushare_limit_type",
"tushare_industry",
"tushare_name",
"tushare_first_time",
"tushare_last_time",
"tushare_up_stat",
]:
if column in normalized_df.columns:
normalized_df[column] = normalized_df[column].astype(str)
return normalized_df
def build_combined_limit_data(daily_df, calc_df, tushare_df):
tushare_columns = [
"tushare_limit_type",
"tushare_industry",
"tushare_name",
"tushare_close",
"tushare_pct_chg",
"tushare_amount",
"tushare_limit_amount",
"tushare_float_mv",
"tushare_total_mv",
"tushare_turnover_ratio",
"tushare_fd_amount",
"tushare_first_time",
"tushare_last_time",
"tushare_open_times",
"tushare_up_stat",
"tushare_limit_times",
]
for column in tushare_columns:
if column not in tushare_df.columns:
tushare_df[column] = None
calc_key_df = pd.DataFrame(columns=["trade_date", "symbol"])
if not calc_df.empty:
calc_key_df = calc_df[["trade_date", "symbol"]].copy()
tushare_key_df = pd.DataFrame(columns=["trade_date", "symbol"])
if not tushare_df.empty:
tushare_key_df = tushare_df[["trade_date", "symbol"]].copy()
keys_df = pd.concat(
[calc_key_df, tushare_key_df],
ignore_index=True,
).drop_duplicates()
if keys_df.empty:
return pd.DataFrame()
for frame in [daily_df, calc_df, tushare_df, keys_df]:
if "trade_date" in frame.columns:
frame["trade_date"] = pd.to_datetime(frame["trade_date"]).dt.date
if "symbol" in frame.columns:
frame["symbol"] = frame["symbol"].astype(str).str.zfill(6)
keys_df = keys_df.drop_duplicates(subset=["trade_date", "symbol"])
canonical_df = daily_df[[
"trade_date",
"symbol",
"ts_code",
"name",
"high",
"close",
"pre_close",
"vol",
"market",
]].drop_duplicates(subset=["trade_date", "symbol"])
combined_df = keys_df.merge(
canonical_df,
how="inner",
on=["trade_date", "symbol"],
)
if combined_df.empty:
return pd.DataFrame()
if not calc_df.empty:
calc_merge_df = calc_df[[
"trade_date",
"symbol",
"status",
"limit_price",
]].rename(columns={
"status": "calc_status",
"limit_price": "calc_limit_price",
})
combined_df = combined_df.merge(
calc_merge_df,
how="left",
on=["trade_date", "symbol"],
)
else:
combined_df["calc_status"] = None
combined_df["calc_limit_price"] = None
if not tushare_df.empty:
tushare_merge_df = tushare_df[
["trade_date", "symbol", *tushare_columns]
].sort_values("tushare_limit_type").drop_duplicates(
subset=["trade_date", "symbol"],
keep="last",
)
combined_df = combined_df.merge(
tushare_merge_df,
how="left",
on=["trade_date", "symbol"],
)
else:
for column in tushare_columns:
combined_df[column] = None
combined_df["calc_high"] = combined_df["high"]
combined_df["calc_close"] = combined_df["close"]
combined_df["calc_pre_close"] = combined_df["pre_close"]
combined_df["calc_vol"] = combined_df["vol"]
combined_df["final_status"] = combined_df.apply(resolve_final_status, axis=1)
combined_df["source_flags"] = combined_df.apply(resolve_source_flags, axis=1)
combined_df["source_count"] = combined_df["source_flags"].apply(
lambda value: 0 if not value else len(value.split(","))
)
combined_df["name"] = combined_df["name"].astype(str).str.strip()
result_df = combined_df[[
"trade_date",
"symbol",
"ts_code",
"name",
"final_status",
"source_count",
"source_flags",
"calc_status",
"calc_limit_price",
"calc_high",
"calc_close",
"calc_pre_close",
"calc_vol",
"market",
*tushare_columns,
]].copy()
result_df = result_df.sort_values(
["trade_date", "ts_code", "source_count"],
ascending=[True, True, False],
)
return result_df.drop_duplicates(subset=["trade_date", "ts_code"], keep="first")
def resolve_final_status(row):
if pd.notna(row.get("calc_status")):
return int(row["calc_status"])
if row.get("tushare_limit_type") == "Z":
return 2
return 1
def resolve_source_flags(row):
flags = []
if pd.notna(row.get("calc_status")):
flags.append("calc")
if pd.notna(row.get("tushare_limit_type")):
flags.append("tushare")
return ",".join(flags)
def analyze_and_save_limits(start_date, end_date):
"""
计算并在数据库中落地涨停、炸板详情
"""
engine = get_engine()
# 0. 初始化检查表结构
print(f"🚀 开始处理涨停/炸板数据:({start_date} -> {end_date}]")
print(f"🧱 初始化并检查融合表 {COMBINED_TABLE} ...")
init_db(engine)
print("✅ 表结构、历史重复数据和唯一索引检查完成。")
requested_start_date = pd.to_datetime(start_date).date()
if requested_start_date < LOCAL_START_DATE:
print(f"ℹ️ 本地 stock_daily 暂按 {LOCAL_START_DATE} 起处理,已忽略更早日期。")
# 1. 获取基础数据和行情数据
query = text("""
SELECT
d.trade_date, d.symbol, d.ts_code, b.name,
d.high, d.close, d.pre_close, d.vol, b.market
FROM stock_daily d
JOIN stock_basic b
ON d.ts_code COLLATE utf8mb4_unicode_ci = b.ts_code COLLATE utf8mb4_unicode_ci
WHERE d.trade_date > :start_date
AND d.trade_date <= :end_date
AND d.trade_date >= :local_start_date
AND d.pre_close IS NOT NULL
ORDER BY d.trade_date ASC
""")
try:
print("📥 读取 stock_daily 与 stock_basic 数据 ...")
df = pd.read_sql(
query,
engine,
params={
"start_date": start_date,
"end_date": end_date,
"local_start_date": LOCAL_START_DATE,
},
)
if df.empty:
print(f"⚠️ 在指定区间 ({start_date} -> {end_date}] 内未获取到数据。")
return True
target_dates = df['trade_date'].astype(str).unique()
print(f"✅ 读取完成:{len(df)} 行行情,覆盖 {len(target_dates)} 个交易日。")
# 2. 涨停价计算规则
def calc_limit(row):
m, p = str(row['market']), float(row['pre_close'])
# 主板10%,创业板/科创板20%,北交所30%
ratio = 1.2 if ('创业板' in m or '科创板' in m) else (1.3 if '北交所' in m else 1.1)
return round(p * ratio, 2)
df['limit_price'] = df.apply(calc_limit, axis=1)
# 3. 筛选涨停(1)和炸板(2)
df['is_limit'] = (df['high'] == df['limit_price']) & (df['close'] == df['limit_price'])
df['is_broken'] = (df['high'] == df['limit_price']) & (df['close'] < df['limit_price'])
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])
print(
f"🧮 本地计算完成:涨停 {len(limit_df)} 条,炸板 {len(broken_df)} 条,"
f"合计 {len(save_df)} 条。"
)
tushare_df = fetch_tushare_limit_list(target_dates)
print("🔗 融合本地计算结果与 Tushare 数据 ...")
combined_df = build_combined_limit_data(df.copy(), save_df.copy(), tushare_df.copy())
validate_no_duplicate_frame_rows(combined_df, "融合待入库数据")
print(f"✅ 融合完成:待入库 {len(combined_df)} 条,重复键检查通过。")
with engine.begin() as conn:
delete_params = {
f"date_{idx}": value for idx, value in enumerate(target_dates)
}
placeholders = ", ".join(f":date_{idx}" for idx in range(len(target_dates)))
combined_delete_query = text(
f"DELETE FROM {COMBINED_TABLE} WHERE trade_date IN ({placeholders})"
)
print(f"🧹 清理 {len(target_dates)} 个交易日的旧融合数据 ...")
delete_result = conn.execute(combined_delete_query, delete_params)
print(f"✅ 旧数据清理完成:删除 {delete_result.rowcount} 条。")
if not combined_df.empty:
print(f"💾 写入 {COMBINED_TABLE}:{len(combined_df)} 条 ...")
combined_df.to_sql(
COMBINED_TABLE,
conn,
if_exists='append',
index=False,
chunksize=1000,
method="multi",
)
else:
print("ℹ️ 融合结果为空,本次不写入新数据。")
validate_no_duplicate_combined_rows(conn)
print("✅ 写入后数据库重复键校验通过。")
if not save_df.empty:
print(f"✅ 处理成功!涉及交易日: {len(target_dates)} 天")
print(f"📊 融合表 {COMBINED_TABLE} 入库: {len(combined_df)} 条记录。")
else:
print(f"ℹ️ 该时段内本地行情无涨停或炸板记录,已清理 {len(target_dates)} 个交易日的融合表旧结果。")
print(f"📊 融合表 {COMBINED_TABLE} 入库: {len(combined_df)} 条记录。")
return True
except Exception as e:
print(f"❌ 运行失败: {e}")
return False
def cleanup_combined_data():
engine = get_engine()
try:
init_db(engine)
print(f"✅ {COMBINED_TABLE} 已完成历史重复数据清理、唯一索引检查和 calc_vol 回填。")
return True
except Exception as e:
print(f"❌ 清理失败: {e}")
return False
# ============================================================
# 用户配置区
# ============================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="计算并保存涨停/炸板数据")
parser.add_argument("--start-date", default="2026-04-30", help="开始日期,格式 YYYY-MM-DD,实际查询为大于该日期")
parser.add_argument("--end-date", default="2026-04-30", help="结束日期,格式 YYYY-MM-DD")
parser.add_argument(
"--cleanup-only",
action="store_true",
help="仅清理 stock_limit_combined_data 历史重复数据并从 stock_daily 回填 calc_vol",
)
args = parser.parse_args()
if args.cleanup_only:
cleanup_combined_data()
raise SystemExit(0)
# 开始日期:计算从该日期的下一个交易日开始
START_DATE = args.start_date
# 结束日期
END_DATE = args.end_date
analyze_and_save_limits(START_DATE, END_DATE)