修改limit_up数据来源

This commit is contained in:
2026-05-05 14:13:11 +08:00
parent 7b8a47ffe6
commit f8395f706e
4 changed files with 507 additions and 222 deletions
+1 -1
View File
@@ -130,7 +130,7 @@ def run(start_date, end_date, token):
# ======================= # =======================
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="同步交易日历") parser = argparse.ArgumentParser(description="同步交易日历")
parser.add_argument("--start-date", default="20260429", help="开始日期,格式 YYYYMMDD") parser.add_argument("--start-date", default="20180101", help="开始日期,格式 YYYYMMDD")
parser.add_argument("--end-date", default="20260430", help="结束日期,格式 YYYYMMDD") parser.add_argument("--end-date", default="20260430", help="结束日期,格式 YYYYMMDD")
args = parser.parse_args() args = parser.parse_args()
+19 -10
View File
@@ -1,5 +1,6 @@
import tushare as ts import tushare as ts
import pymysql import pymysql
import pandas as pd
from datetime import datetime from datetime import datetime
import time import time
import argparse import argparse
@@ -53,6 +54,14 @@ def str_to_date(date_str):
return datetime.strptime(date_str, "%Y%m%d").date() return datetime.strptime(date_str, "%Y%m%d").date()
def mysql_value(value):
if pd.isna(value):
return None
if hasattr(value, "item"):
return value.item()
return value
# ===== 获取交易日 ===== # ===== 获取交易日 =====
def get_trade_dates(start_date, end_date): def get_trade_dates(start_date, end_date):
_, db_cursor = get_connection() _, db_cursor = get_connection()
@@ -90,15 +99,15 @@ def insert_data(df):
ts_code, ts_code,
symbol, symbol,
str_to_date(row['trade_date']), # 保持 DATE 类型 str_to_date(row['trade_date']), # 保持 DATE 类型
row['open'], mysql_value(row['open']),
row['high'], mysql_value(row['high']),
row['low'], mysql_value(row['low']),
row['close'], mysql_value(row['close']),
row['pre_close'], mysql_value(row['pre_close']),
row['change'], mysql_value(row['change']),
row['pct_chg'], mysql_value(row['pct_chg']),
row['vol'], mysql_value(row['vol']),
row['amount'] mysql_value(row['amount'])
)) ))
db_cursor.executemany(sql, data) db_cursor.executemany(sql, data)
@@ -148,7 +157,7 @@ def run(start_date, end_date):
# ===== 输入参数 ===== # ===== 输入参数 =====
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="同步日行情") parser = argparse.ArgumentParser(description="同步日行情")
parser.add_argument("--start-date", default="20260430", help="开始日期,格式 YYYYMMDD") parser.add_argument("--start-date", default="20180102", help="开始日期,格式 YYYYMMDD")
parser.add_argument("--end-date", default="20260430", help="结束日期,格式 YYYYMMDD") parser.add_argument("--end-date", default="20260430", help="结束日期,格式 YYYYMMDD")
args = parser.parse_args() args = parser.parse_args()
+473 -208
View File
@@ -1,202 +1,333 @@
import pandas as pd import pandas as pd
from sqlalchemy import text from sqlalchemy import text
import argparse import argparse
import re import tushare as ts
from datetime import date
from stock_config import get_engine from stock_config import TUSHARE_TOKEN, get_engine
# --- 数据库配置 --- # --- 数据库配置 ---
# 请根据实际情况修改数据库连接信息 # 请根据实际情况修改数据库连接信息
# DB_CONN_STR = 'mysql+pymysql://root:password@127.0.0.1:3306/stock_data' # DB_CONN_STR = 'mysql+pymysql://root:password@127.0.0.1:3306/stock_data'
AK_START_DATE = pd.to_datetime("2026-04-01").date() LOCAL_START_DATE = date(2018, 1, 1)
TUSHARE_LIMIT_LIST_START_DATE = date(2020, 1, 1)
COMBINED_TABLE = "stock_limit_combined_data"
def normalize_stock_name(value): def normalize_trade_dates(trade_dates):
if pd.isna(value): return sorted({pd.to_datetime(trade_date).date() for trade_date in trade_dates})
return ""
text_value = str(value).upper()
text_value = re.sub(r"\s+", "", text_value)
text_value = re.sub(r"[\**·・.。,_,、/\\()()\[\]【】\-—_]+", "", text_value)
for marker in ["行权", "除权", "复权"]: def filter_dates_by_start(trade_dates, start_date, source_name):
text_value = text_value.replace(marker, "") normalized_dates = normalize_trade_dates(trade_dates)
filtered_dates = [trade_date for trade_date in normalized_dates if trade_date >= start_date]
return text_value 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): def init_db(engine):
""" """
初始化数据库表结构 初始化数据库表结构
""" """
create_table_sql = """ with engine.begin() as conn:
CREATE TABLE IF NOT EXISTS `stock_limit_data` ( 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, `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`trade_date` DATE NOT NULL COMMENT '交易日期', `trade_date` DATE NOT NULL COMMENT '交易日期',
`symbol` VARCHAR(10) NOT NULL COMMENT '股票代码', `symbol` VARCHAR(10) NOT NULL COMMENT '股票代码,以stock_daily为准',
`ts_code` VARCHAR(20) NOT NULL COMMENT 'TS股票代码', `ts_code` VARCHAR(20) NOT NULL COMMENT 'TS股票代码,以stock_daily为准',
`name` VARCHAR(50) NOT NULL COMMENT '股票名称', `name` VARCHAR(50) NOT NULL COMMENT '股票名称,以stock_daily关联基础表为准',
`status` TINYINT NOT NULL COMMENT '状态:1-涨停,2-炸板', `final_status` TINYINT NOT NULL COMMENT '融合状态:1-涨停,2-炸板',
`limit_price` DECIMAL(16, 4) COMMENT '计算出的理论涨停价', `source_count` INT NOT NULL DEFAULT 0 COMMENT '命中的数据源数量',
`close` DECIMAL(16, 4) COMMENT '实际收盘价', `source_flags` VARCHAR(50) COMMENT '命中的数据源:calc,tushare',
`vol` BIGINT COMMENT '成交量', `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前收盘价',
`vol` BIGINT COMMENT 'stock_daily成交量',
`calc_vol` BIGINT COMMENT 'stock_daily成交量',
`market` VARCHAR(20) COMMENT '所属板块', `market` VARCHAR(20) COMMENT '所属板块',
`ak_code` VARCHAR(10) COMMENT 'AKShare-代码', `tushare_limit_type` VARCHAR(1) COMMENT 'Tushare-D跌停U涨停Z炸板',
`ak_name` VARCHAR(50) COMMENT 'AKShare-名称', `tushare_industry` VARCHAR(100) COMMENT 'Tushare-所属行业',
`ak_pct_chg` DECIMAL(10, 4) COMMENT 'AKShare-涨跌幅', `tushare_name` VARCHAR(50) COMMENT 'Tushare-名称',
`ak_latest_price` DECIMAL(16, 4) COMMENT 'AKShare-最新价', `tushare_close` DECIMAL(16, 4) COMMENT 'Tushare-收盘价',
`ak_amount` DECIMAL(24, 4) COMMENT 'AKShare-成交额', `tushare_pct_chg` DECIMAL(10, 4) COMMENT 'Tushare-涨跌幅',
`ak_float_market_value` DECIMAL(24, 4) COMMENT 'AKShare-流通市值', `tushare_amount` DECIMAL(24, 4) COMMENT 'Tushare-成交额',
`ak_total_market_value` DECIMAL(24, 4) COMMENT 'AKShare-总市值', `tushare_limit_amount` DECIMAL(24, 4) COMMENT 'Tushare-板上成交金额',
`ak_turnover_rate` DECIMAL(10, 4) COMMENT 'AKShare-换手率', `tushare_float_mv` DECIMAL(24, 4) COMMENT 'Tushare-流通市值',
`ak_seal_amount` DECIMAL(24, 4) COMMENT 'AKShare-封板资金', `tushare_total_mv` DECIMAL(24, 4) COMMENT 'Tushare-总市值',
`ak_first_limit_time` VARCHAR(20) COMMENT 'AKShare-首次封板时间', `tushare_turnover_ratio` DECIMAL(10, 4) COMMENT 'Tushare-换手率',
`ak_last_limit_time` VARCHAR(20) COMMENT 'AKShare-最后封板时间', `tushare_fd_amount` DECIMAL(24, 4) COMMENT 'Tushare-封单金额',
`ak_break_limit_count` INT COMMENT 'AKShare-炸板次数', `tushare_first_time` VARCHAR(20) COMMENT 'Tushare-首次封板时间',
`ak_limit_up_stat` VARCHAR(50) COMMENT 'AKShare-涨停统计', `tushare_last_time` VARCHAR(20) COMMENT 'Tushare-最后封板时间',
`ak_limit_up_days` INT COMMENT 'AKShare-连板数', `tushare_open_times` INT COMMENT 'Tushare-炸板/开板次数',
`ak_industry` VARCHAR(100) COMMENT 'AKShare-所属行业', `tushare_up_stat` VARCHAR(50) COMMENT 'Tushare-涨停统计',
UNIQUE KEY `uniq_date_code` (`trade_date`, `ts_code`) `tushare_limit_times` INT COMMENT 'Tushare-连板数',
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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;
""" """
with engine.begin() as conn:
conn.execute(text(create_table_sql))
ensure_base_columns(conn)
ensure_ak_columns(conn)
def ensure_base_columns(conn): def get_table_columns(conn, table_name):
existing_columns = { return {
row[0] row[0]
for row in conn.execute(text("SHOW COLUMNS FROM `stock_limit_data`")).fetchall() for row in conn.execute(text(f"SHOW COLUMNS FROM `{table_name}`")).fetchall()
}
base_columns = {
"vol": "BIGINT COMMENT '成交量'",
} }
for column_name, column_def in base_columns.items():
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 ensure_combined_columns(conn):
existing_columns = get_table_columns(conn, COMBINED_TABLE)
columns = {
"vol": "BIGINT COMMENT 'stock_daily成交量'",
"calc_vol": "BIGINT COMMENT 'stock_daily成交量'",
}
for column_name, column_def in columns.items():
if column_name not in existing_columns: if column_name not in existing_columns:
conn.execute(text(f"ALTER TABLE `stock_limit_data` ADD COLUMN `{column_name}` {column_def}")) after_column = "calc_pre_close" if column_name == "vol" else "vol"
conn.execute(
text(
f"ALTER TABLE `{COMBINED_TABLE}` "
f"ADD COLUMN `{column_name}` {column_def} AFTER `{after_column}`"
)
)
def ensure_ak_columns(conn): def sync_combined_vol_from_daily(conn):
existing_columns = { conn.execute(
row[0] text(
for row in conn.execute(text("SHOW COLUMNS FROM `stock_limit_data`")).fetchall() f"""
} UPDATE `{COMBINED_TABLE}` c
ak_columns = { JOIN `stock_daily` d
"ak_code": "VARCHAR(10) COMMENT 'AKShare-代码'", ON c.trade_date = d.trade_date
"ak_name": "VARCHAR(50) COMMENT 'AKShare-名称'", AND c.ts_code COLLATE utf8mb4_unicode_ci = d.ts_code COLLATE utf8mb4_unicode_ci
"ak_pct_chg": "DECIMAL(10, 4) COMMENT 'AKShare-涨跌幅'", SET c.symbol = d.symbol,
"ak_latest_price": "DECIMAL(16, 4) COMMENT 'AKShare-最新价'", c.vol = d.vol,
"ak_amount": "DECIMAL(24, 4) COMMENT 'AKShare-成交额'", c.calc_vol = d.vol
"ak_float_market_value": "DECIMAL(24, 4) COMMENT 'AKShare-流通市值'", """
"ak_total_market_value": "DECIMAL(24, 4) COMMENT 'AKShare-总市值'", )
"ak_turnover_rate": "DECIMAL(10, 4) COMMENT 'AKShare-换手率'", )
"ak_seal_amount": "DECIMAL(24, 4) COMMENT 'AKShare-封板资金'",
"ak_first_limit_time": "VARCHAR(20) COMMENT 'AKShare-首次封板时间'",
"ak_last_limit_time": "VARCHAR(20) COMMENT 'AKShare-最后封板时间'",
"ak_break_limit_count": "INT COMMENT 'AKShare-炸板次数'",
"ak_limit_up_stat": "VARCHAR(50) COMMENT 'AKShare-涨停统计'",
"ak_limit_up_days": "INT COMMENT 'AKShare-连板数'",
"ak_industry": "VARCHAR(100) COMMENT 'AKShare-所属行业'",
}
for column_name, column_def in ak_columns.items():
if column_name not in existing_columns:
conn.execute(text(f"ALTER TABLE `stock_limit_data` ADD COLUMN `{column_name}` {column_def}"))
def fetch_ak_limit_pool(trade_dates): def dedupe_combined_table(conn):
try: conn.execute(
import akshare as ak text(
except ImportError as exc: f"""
raise RuntimeError("缺少 akshare 依赖,请先安装: pip install akshare") from exc 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.vol IS NOT NULL OR 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.vol IS NOT NULL OR 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.vol IS NOT NULL OR 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.vol IS NOT NULL OR c2.calc_vol IS NOT NULL, 1, 0)
)
AND c1.id < c2.id
)
"""
)
)
def ensure_combined_indexes(conn):
dedupe_combined_table(conn)
if not index_exists(conn, COMBINED_TABLE, "uniq_date_code"):
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)
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:
return pd.DataFrame(columns=["trade_date", "symbol", "ts_code"])
ts.set_token(TUSHARE_TOKEN)
pro = ts.pro_api()
frames = [] frames = []
for trade_date in trade_dates: fields = (
trade_day = pd.to_datetime(trade_date).date() "trade_date,ts_code,industry,name,close,pct_chg,amount,limit_amount,"
if trade_day < AK_START_DATE: "float_mv,total_mv,turnover_ratio,fd_amount,first_time,last_time,"
continue "open_times,up_stat,limit_times,limit"
)
for trade_date in tushare_trade_dates:
tushare_date = pd.to_datetime(trade_date).strftime("%Y%m%d")
for limit_type in ("U", "Z"):
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
ak_date = pd.to_datetime(trade_date).strftime("%Y%m%d") if ts_df is None or ts_df.empty:
try: continue
ak_df = ak.stock_zt_pool_em(date=ak_date)
except Exception as exc:
print(f"⚠️ AKShare 涨停池获取失败: {ak_date}, 错误: {exc}")
continue
if ak_df is None or ak_df.empty: normalized_df = normalize_tushare_limit_list(ts_df)
continue if not normalized_df.empty:
frames.append(normalized_df)
normalized_df = normalize_ak_limit_pool(ak_df, trade_date)
if not normalized_df.empty:
frames.append(normalized_df)
if not frames: if not frames:
return pd.DataFrame(columns=["trade_date", "symbol", "name"]) return pd.DataFrame(columns=["trade_date", "symbol", "ts_code"])
return pd.concat(frames, ignore_index=True) return pd.concat(frames, ignore_index=True).drop_duplicates(
subset=["trade_date", "ts_code", "tushare_limit_type"],
keep="first",
)
def normalize_ak_limit_pool(ak_df, trade_date): def normalize_tushare_limit_list(ts_df):
column_map = { column_map = {
"代码": "ak_code", "limit": "tushare_limit_type",
"名称": "ak_name", "industry": "tushare_industry",
"涨跌幅": "ak_pct_chg", "name": "tushare_name",
"最新价": "ak_latest_price", "close": "tushare_close",
"成交额": "ak_amount", "pct_chg": "tushare_pct_chg",
"流通市值": "ak_float_market_value", "amount": "tushare_amount",
"总市值": "ak_total_market_value", "limit_amount": "tushare_limit_amount",
"换手率": "ak_turnover_rate", "float_mv": "tushare_float_mv",
"封板资金": "ak_seal_amount", "total_mv": "tushare_total_mv",
"首次封板时间": "ak_first_limit_time", "turnover_ratio": "tushare_turnover_ratio",
"最后封板时间": "ak_last_limit_time", "fd_amount": "tushare_fd_amount",
"炸板次数": "ak_break_limit_count", "first_time": "tushare_first_time",
"涨停统计": "ak_limit_up_stat", "last_time": "tushare_last_time",
"连板数": "ak_limit_up_days", "open_times": "tushare_open_times",
"所属行业": "ak_industry", "up_stat": "tushare_up_stat",
"limit_times": "tushare_limit_times",
} }
missing_columns = [column for column in column_map if column not in ak_df.columns] required_columns = {"trade_date", "ts_code", "limit"}
if missing_columns: if not required_columns.issubset(ts_df.columns):
print(f"⚠️ AKShare 返回字段缺失,已跳过缺失字段: {', '.join(missing_columns)}") missing_columns = sorted(required_columns - set(ts_df.columns))
print(f"⚠️ Tushare 返回字段缺失,已跳过: {', '.join(missing_columns)}")
if "代码" not in ak_df.columns or "名称" not in ak_df.columns: return pd.DataFrame(columns=["trade_date", "symbol", "ts_code"])
return pd.DataFrame(columns=["trade_date", "symbol", "name"])
available_map = { available_map = {
source: target for source, target in column_map.items() if source in ak_df.columns source: target for source, target in column_map.items() if source in ts_df.columns
} }
normalized_df = ak_df[list(available_map)].rename(columns=available_map).copy() normalized_df = ts_df[["trade_date", "ts_code", *available_map]].rename(
normalized_df["trade_date"] = pd.to_datetime(trade_date).date() columns=available_map
normalized_df["symbol"] = ( ).copy()
normalized_df["ak_code"].astype(str).str.extract(r"(\d+)", expand=False).str.zfill(6) normalized_df["trade_date"] = pd.to_datetime(
) normalized_df["trade_date"], format="%Y%m%d"
normalized_df["name"] = normalized_df["ak_name"].astype(str).str.strip() ).dt.date
normalized_df["normalized_name"] = normalized_df["name"].apply(normalize_stock_name) 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 [ for column in [
"ak_pct_chg", "tushare_close",
"ak_latest_price", "tushare_pct_chg",
"ak_amount", "tushare_amount",
"ak_float_market_value", "tushare_limit_amount",
"ak_total_market_value", "tushare_float_mv",
"ak_turnover_rate", "tushare_total_mv",
"ak_seal_amount", "tushare_turnover_ratio",
"ak_break_limit_count", "tushare_fd_amount",
"ak_limit_up_days", "tushare_open_times",
"tushare_limit_times",
]: ]:
if column in normalized_df.columns: if column in normalized_df.columns:
normalized_df[column] = pd.to_numeric(normalized_df[column], errors="coerce") normalized_df[column] = pd.to_numeric(normalized_df[column], errors="coerce")
for column in [ for column in [
"ak_code", "tushare_limit_type",
"ak_name", "tushare_industry",
"ak_first_limit_time", "tushare_name",
"ak_last_limit_time", "tushare_first_time",
"ak_limit_up_stat", "tushare_last_time",
"ak_industry", "tushare_up_stat",
]: ]:
if column in normalized_df.columns: if column in normalized_df.columns:
normalized_df[column] = normalized_df[column].astype(str) normalized_df[column] = normalized_df[column].astype(str)
@@ -204,54 +335,158 @@ def normalize_ak_limit_pool(ak_df, trade_date):
return normalized_df return normalized_df
def merge_ak_limit_pool(final_df, ak_df): def build_combined_limit_data(daily_df, calc_df, tushare_df):
ak_columns = [ tushare_columns = [
"ak_code", "tushare_limit_type",
"ak_name", "tushare_industry",
"ak_pct_chg", "tushare_name",
"ak_latest_price", "tushare_close",
"ak_amount", "tushare_pct_chg",
"ak_float_market_value", "tushare_amount",
"ak_total_market_value", "tushare_limit_amount",
"ak_turnover_rate", "tushare_float_mv",
"ak_seal_amount", "tushare_total_mv",
"ak_first_limit_time", "tushare_turnover_ratio",
"ak_last_limit_time", "tushare_fd_amount",
"ak_break_limit_count", "tushare_first_time",
"ak_limit_up_stat", "tushare_last_time",
"ak_limit_up_days", "tushare_open_times",
"ak_industry", "tushare_up_stat",
"tushare_limit_times",
] ]
for column in ak_columns:
if column not in ak_df.columns:
ak_df[column] = None
final_df["normalized_name"] = final_df["name"].apply(normalize_stock_name) for column in tushare_columns:
if "normalized_name" not in ak_df.columns: if column not in tushare_df.columns:
ak_df["normalized_name"] = ak_df["name"].apply(normalize_stock_name) tushare_df[column] = None
merge_keys = ["trade_date", "symbol"] calc_key_df = pd.DataFrame(columns=["trade_date", "symbol"])
ak_merge_df = ak_df[ if not calc_df.empty:
merge_keys + ["normalized_name"] + ak_columns calc_key_df = calc_df[["trade_date", "symbol"]].copy()
].drop_duplicates(subset=merge_keys) tushare_key_df = pd.DataFrame(columns=["trade_date", "symbol"])
merged_df = final_df.merge( if not tushare_df.empty:
ak_merge_df, tushare_key_df = tushare_df[["trade_date", "symbol"]].copy()
how="left",
on=merge_keys, keys_df = pd.concat(
suffixes=("", "_ak_match"), [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"],
) )
name_mismatch = ( if combined_df.empty:
merged_df["ak_code"].notna() return pd.DataFrame()
& merged_df["normalized_name_ak_match"].notna()
& (merged_df["normalized_name"] != merged_df["normalized_name_ak_match"])
)
if name_mismatch.any():
print(f"⚠️ 有 {int(name_mismatch.sum())} 条记录代码一致但名称不一致,已按代码合并 AKShare 字段。")
return merged_df[[ if not calc_df.empty:
"trade_date", "symbol", "ts_code", "name", "status", "limit_price", "close", "vol", "market", calc_merge_df = calc_df[[
*ak_columns, "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["vol"] = combined_df["vol"]
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",
"vol",
"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): def analyze_and_save_limits(start_date, end_date):
@@ -263,15 +498,21 @@ def analyze_and_save_limits(start_date, end_date):
# 0. 初始化检查表结构 # 0. 初始化检查表结构
init_db(engine) init_db(engine)
requested_start_date = pd.to_datetime(start_date).date()
if requested_start_date < LOCAL_START_DATE:
print(f"ℹ️ 本地 stock_daily 暂按 {LOCAL_START_DATE} 起处理,已忽略更早日期。")
# 1. 获取基础数据和行情数据 # 1. 获取基础数据和行情数据
query = text(""" query = text("""
SELECT SELECT
d.trade_date, d.symbol, d.ts_code, b.name, d.trade_date, d.symbol, d.ts_code, b.name,
d.high, d.close, d.pre_close, d.vol, b.market d.high, d.close, d.pre_close, d.vol, b.market
FROM stock_daily d FROM stock_daily d
JOIN stock_basic b ON d.ts_code = b.ts_code 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 WHERE d.trade_date > :start_date
AND d.trade_date <= :end_date AND d.trade_date <= :end_date
AND d.trade_date >= :local_start_date
AND d.pre_close IS NOT NULL AND d.pre_close IS NOT NULL
ORDER BY d.trade_date ASC ORDER BY d.trade_date ASC
""") """)
@@ -280,7 +521,11 @@ def analyze_and_save_limits(start_date, end_date):
df = pd.read_sql( df = pd.read_sql(
query, query,
engine, engine,
params={"start_date": start_date, "end_date": end_date}, params={
"start_date": start_date,
"end_date": end_date,
"local_start_date": LOCAL_START_DATE,
},
) )
if df.empty: if df.empty:
print(f"⚠️ 在指定区间 ({start_date} -> {end_date}] 内未获取到数据。") print(f"⚠️ 在指定区间 ({start_date} -> {end_date}] 内未获取到数据。")
@@ -308,35 +553,35 @@ def analyze_and_save_limits(start_date, end_date):
save_df = pd.concat([limit_df, broken_df]) save_df = pd.concat([limit_df, broken_df])
target_dates = df['trade_date'].astype(str).unique() target_dates = df['trade_date'].astype(str).unique()
ak_df = fetch_ak_limit_pool(target_dates) tushare_df = fetch_tushare_limit_list(target_dates)
combined_df = build_combined_limit_data(df.copy(), save_df.copy(), tushare_df.copy())
with engine.begin() as conn: with engine.begin() as conn:
delete_params = { delete_params = {
f"date_{idx}": value for idx, value in enumerate(target_dates) f"date_{idx}": value for idx, value in enumerate(target_dates)
} }
placeholders = ", ".join(f":date_{idx}" for idx in range(len(target_dates))) placeholders = ", ".join(f":date_{idx}" for idx in range(len(target_dates)))
delete_query = text( combined_delete_query = text(
f"DELETE FROM stock_limit_data WHERE trade_date IN ({placeholders})" f"DELETE FROM {COMBINED_TABLE} WHERE trade_date IN ({placeholders})"
) )
conn.execute(delete_query, delete_params) conn.execute(combined_delete_query, delete_params)
if not save_df.empty: if not combined_df.empty:
final_df = save_df[[ combined_df.to_sql(
'trade_date', 'symbol', 'ts_code', 'name', COMBINED_TABLE,
'status', 'limit_price', 'close', 'vol', 'market' conn,
]].copy() if_exists='append',
final_df["trade_date"] = pd.to_datetime(final_df["trade_date"]).dt.date index=False,
final_df["symbol"] = final_df["symbol"].astype(str).str.zfill(6) chunksize=1000,
final_df["name"] = final_df["name"].astype(str).str.strip() method="multi",
final_df = merge_ak_limit_pool(final_df, ak_df) )
final_df.to_sql('stock_limit_data', conn, if_exists='append', index=False)
if not save_df.empty: if not save_df.empty:
matched_count = int(final_df["ak_code"].notna().sum())
print(f"✅ 处理成功!涉及交易日: {len(target_dates)} 天") print(f"✅ 处理成功!涉及交易日: {len(target_dates)} 天")
print(f"📊 总计入库: {len(final_df)} 条记录,AKShare 匹配补充: {matched_count} 条。") print(f"📊 融合表 {COMBINED_TABLE} 入库: {len(combined_df)} 条记录。")
else: else:
print(f"ℹ️ 该时段内全市场无涨停或炸板记录,已清理 {len(target_dates)} 个交易日的旧结果。") print(f"ℹ️ 该时段内本地行情无涨停或炸板记录,已清理 {len(target_dates)} 个交易日的融合表旧结果。")
print(f"📊 融合表 {COMBINED_TABLE} 入库: {len(combined_df)} 条记录。")
return True return True
except Exception as e: except Exception as e:
@@ -344,15 +589,35 @@ def analyze_and_save_limits(start_date, end_date):
return False return False
def cleanup_combined_data():
engine = get_engine()
try:
init_db(engine)
print(f"✅ {COMBINED_TABLE} 已完成历史重复数据清理、唯一索引检查和 vol 回填。")
return True
except Exception as e:
print(f"❌ 清理失败: {e}")
return False
# ============================================================ # ============================================================
# 用户配置区 # 用户配置区
# ============================================================ # ============================================================
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="计算并保存涨停/炸板数据") parser = argparse.ArgumentParser(description="计算并保存涨停/炸板数据")
parser.add_argument("--start-date", default="2026-04-29", help="开始日期,格式 YYYY-MM-DD,实际查询为大于该日期") parser.add_argument("--start-date", default="2026-03-02", help="开始日期,格式 YYYY-MM-DD,实际查询为大于该日期")
parser.add_argument("--end-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 回填 vol",
)
args = parser.parse_args() args = parser.parse_args()
if args.cleanup_only:
cleanup_combined_data()
raise SystemExit(0)
# 开始日期:计算从该日期的下一个交易日开始 # 开始日期:计算从该日期的下一个交易日开始
START_DATE = args.start_date START_DATE = args.start_date
+14 -3
View File
@@ -6,11 +6,22 @@ TUSHARE_TOKEN = os.getenv(
"0fad3cf498757089e2630028455d5bbe1637475788bcdaa1f2175e93", "0fad3cf498757089e2630028455d5bbe1637475788bcdaa1f2175e93",
) )
# 阿里云远程数据库
# DB_CONFIG = {
# "host": os.getenv("MYSQL_HOST", "8.159.129.156"),
# "port": int(os.getenv("MYSQL_PORT", "10836")),
# "user": os.getenv("MYSQL_USER", "yangfan"),
# "password": os.getenv("MYSQL_PASSWORD", "aA%8023321088"),
# "database": os.getenv("MYSQL_DATABASE", "level"),
# "charset": os.getenv("MYSQL_CHARSET", "utf8mb4"),
# }
# mac本地mysql
DB_CONFIG = { DB_CONFIG = {
"host": os.getenv("MYSQL_HOST", "8.159.129.156"), "host": os.getenv("MYSQL_HOST", "localhost"),
"port": int(os.getenv("MYSQL_PORT", "10836")), "port": int(os.getenv("MYSQL_PORT", "3306")),
"user": os.getenv("MYSQL_USER", "yangfan"), "user": os.getenv("MYSQL_USER", "yangfan"),
"password": os.getenv("MYSQL_PASSWORD", "aA%8023321088"), "password": os.getenv("MYSQL_PASSWORD", "admin"),
"database": os.getenv("MYSQL_DATABASE", "level"), "database": os.getenv("MYSQL_DATABASE", "level"),
"charset": os.getenv("MYSQL_CHARSET", "utf8mb4"), "charset": os.getenv("MYSQL_CHARSET", "utf8mb4"),
} }