A股风险警示数据清洗:从交易所公告到日度面板的Stata实践 1. 项目概述为什么你需要一份干净的A股风险警示数据做A股研究或者量化策略的朋友肯定都踩过“ST股”这个坑。你可能花了大量时间构建了一个漂亮的因子模型回测曲线非常完美结果一上实盘突然因为持仓里混进了一只刚被“ST”的股票导致流动性枯竭、连续跌停策略净值瞬间出现一个大坑。这种经历痛过一次就忘不了。问题的核心在于很多常用的金融数据库无论是免费的还是付费的在提供日频行情、财务数据时并不会自动、清晰地将股票是否处于风险警示状态作为一个可直接筛选的字段给你。你需要自己从公司公告、交易所文件里把这条信息“挖”出来并整理成一份和你的交易日期、股票代码能精确匹配的面板数据。这就是我花时间整理“A股是否ST、*ST或PT数据”的初衷。它不是什么高深的模型而是一份基础设施级别的“清洁数据”。这份数据整理了从A股市场有风险警示制度以来到2023年底所有A股上市公司是否被标记为ST、*ST或PT现已基本退出历史舞台的日度状态面板。更重要的是我附上了完整的Stata整理代码。你拿到的不只是一份“鱼”更是“渔”。你可以用这份数据直接清洗你的样本也可以通过学习代码掌握如何从原始公告中持续更新这份数据甚至举一反三去整理其他类似的“事件型”数据比如股权激励、定向增发、重要股东减持等。简单说这份资源能帮你规避陷阱在回测和实盘中一键排除或特别关注风险警示股票避免“踩雷”。提高效率省去自己从零开始爬取、整理交易所公告的繁琐过程可能节省你数周时间。学习方法通过Stata代码学习金融数据处理中的关键技巧包括文本解析、日期匹配、面板数据构建等。无论你是金融专业的学生、刚入行的研究员还是自己琢磨量化交易的投资者这份数据与代码都能成为你工具箱里一件实用趁手的工具。2. 数据来源与原始材料解析一份可靠的数据产品源头必须清晰、可追溯、可验证。我构建这份数据集的核心原材料只有一类上海证券交易所和深圳证券交易所官方发布的上市公司公告。具体来说是那些标题中包含“关于公司股票实施退市风险警示暨停牌的公告”、“关于撤销退市风险警示并继续实施其他风险警示的公告”等关键字的公告文件。2.1 为什么选择交易所公告作为唯一来源市面上有些数据商会提供“ST状态”字段但其更新及时性、历史准确性有时存疑。还有一些方法是通过股票简称是否包含“ST”或“*ST”来判断这个方法问题很大滞后性公司发布警示公告后其股票简称通常要等到下一个交易日甚至更晚才会变更。如果你在简称变更前就需要判断此方法失效。不准确历史上存在“ST”后又“摘帽”但简称变更延迟的情况。且“PT”制度特别转让时期股票简称并不一定包含“PT”。无法区分ST和*ST对于策略而言*ST退市风险警示和ST其他风险警示的风险等级是不同的仅看简称无法精确区分。因此最权威、最及时、最准确的源头就是交易所官网的公告原文。公告的发布日期精确到日且明确了状态变化的生效日期这为我们构建日度面板数据提供了唯一可信的基础。2.2 原始公告的信息提取关键点一份典型的风险警示公告我们需要从中提取出以下结构化信息股票代码公告主体的6位数字代码。公告日期公告发布的日期。生效日期股票状态开始发生变化的日期通常是公告日后的下一个交易日。事件类型具体是“实施退市风险警示*ST”、“实施其他风险警示ST”、“撤销退市风险警示摘星”、“撤销其他风险警示摘帽”还是“终止上市”等。变更前状态与变更后状态明确状态切换的起点和终点。注意公告的文本格式并非完全统一早期公告和近期公告的表述方式有差异不同券商的模板也不同。因此代码不能依赖简单的关键字定位必须设计能够容错的文本解析逻辑。2.3 辅助数据全A股股票列表与交易日历仅有事件公告是不够的我们需要将其拓展成一份完整的、涵盖所有股票、所有交易日的面板数据。这就需要另外两份基础数据全量股票列表包含历史上所有曾上市A股的代码、简称、上市日期、退市日期若有。用于确定在每一个时点有哪些股票是存在的。交易日历沪深交易所的交易日历。我们的状态数据只需要在交易日有定义即可。整理的核心逻辑就是以“股票-交易日”为二维坐标用公告中提取的“生效日期”和“事件类型”作为“戳”去填充这个二维表格中每一个单元格的状态值。3. Stata整理代码核心模块拆解附带的Stata do文件是整套流程的自动化实现。它不是一个简单的脚本而是一个包含多个模块、具备较强鲁棒性的数据处理程序。下面我拆解几个最核心的模块解释其设计思路和代码技巧。3.1 模块一原始公告文本的解析与清洗这是最难也是最关键的一步。我们假设你已经通过爬虫或手动下载获得了所有相关公告的文本内容并保存在一个变量里比如叫content。* 示例提取股票代码假设代码在公告中以“证券代码”开头 gen code ustrregexs(1) if ustrregexm(content, 证券代码(\d{6})) * 使用Unicode正则表达式更稳健地处理中文文本 * 示例提取公告日期 gen announce_date date(ustrregexs(1), YMD) if ustrregexm(content, 公告日期(\d{4})年(\d{1,2})月(\d{1,2})日) format announce_date %tdCCYY-NN-DD * 示例判断事件类型 - 采用多条件判断避免遗漏 gen event_type replace event_type 实施退市风险警示 if strpos(content, 实施退市风险警示) 0 strpos(content, 撤销) 0 replace event_type 撤销退市风险警示 if strpos(content, 撤销退市风险警示) 0 strpos(content, 继续实施) 0 replace event_type 实施其他风险警示 if strpos(content, 实施其他风险警示) 0 strpos(content, 退市) 0 * ... 其他类型判断实操心得文本解析没有“银弹”。我的经验是先用一批已知的公告样本训练你的正则表达式和判断逻辑覆盖不同年份、不同券商的格式。然后对全量数据运行后一定要对event_type为空或提取日期异常比如早于1990年的观测进行人工抽样检查不断修正你的解析规则。这是一个迭代的过程。3.2 模块二从“事件表”到“状态面板表”假设我们通过模块一得到了一个纯净的“事件表”event_dt包含code代码、effect_date生效日期、event_type事件类型等关键字段。下一步是生成一个包含所有股票、所有交易日的“空白”面板表panel_dt然后让事件去填充它。* 步骤1生成所有日期和所有代码的笛卡尔积使用cross命令或循环合并 * 这里假设已有交易日历表trading_calendar和股票列表表stock_list use trading_calendar, clear rename date trading_date tempfile dates save dates‘ use stock_list, clear tempfile stocks save stocks‘ cross using dates‘ sort code trading_date save all_panel.dta, replace * 步骤2为面板数据生成状态变量并初始化为“正常” use all_panel.dta, clear gen status 正常 // 默认状态3.3 模块三状态填充的逻辑与循环这是最核心的算法部分。我们需要遍历“事件表”里的每一条记录去“面板表”中修改对应股票在对应生效日期及之后的所有日期的状态。* 首先将事件表处理成状态转换规则 use event_dt, clear * 根据event_type生成status_from和status_to gen status_from “正常” gen status_to “正常” replace status_to “ST” if event_type “实施其他风险警示” replace status_to “*ST” if event_type “实施退市风险警示” replace status_from “*ST” if event_type “撤销退市风险警示” replace status_to “正常” if event_type “撤销退市风险警示” * ... 处理所有转换规则 keep code effect_date status_from status_to save transition_rules.dta, replace * 然后使用循环进行状态填充此处展示逻辑实际代码需优化效率 use all_panel.dta, clear merge m:1 code using transition_rules.dta, keepusing(effect_date status_from status_to) sort code trading_date * 初始化一个标记变量 gen state_changed 0 gen current_status “正常” * 按股票代码分组在组内按日期顺序处理此处为伪代码逻辑说明 * 实际Stata实现通常使用 by code (trading_date): 配合 replace 和条件判断 * 核心是当遇到一个 effect_date 时检查当前的 current_status 是否等于 status_from * 如果相等则将从这个 effect_date 开始往后的 current_status 替换为 status_to * 直到遇到下一个匹配的状态转换事件为止。注意事项这个填充逻辑必须严谨处理“连续事件”。比如股票从“正常”-“ST”-“*ST”-“正常”。代码必须确保状态是基于前序状态顺序转换的不能跳步或覆盖错误。我采用的是一种“状态机”的思维为每一只股票维护一个随时间线变化的状态变量。3.4 模块四数据校验与完整性检查生成数据后绝不能直接使用。必须进行多维度校验。自查逻辑一致性检查是否存在同一只股票在同一天有多个矛盾的状态事件如既被ST又被摘帽。这可能是原始公告解析错误。对照已知案例选取一些历史上著名的ST/*ST公司如乐视网、长生生物手动核对数据集中其状态变化的时间点是否与公开报道一致。边界检查检查股票上市首日或退市最后交易日附近的状态是否合理。汇总统计按年度统计ST/*ST公司的数量与学术论文或权威统计报告中的大致趋势进行比对看是否存在数量级上的差异。我的代码中包含了一个validate_data模块会自动输出这些检查的报告提示可能存在问题的股票代码和日期区间供人工复核。4. 最终数据集的结构与应用示例经过上述流程我们得到的是一个长格式long format的面板数据文件st_status_panel.dta其核心结构如下变量名类型说明codestring股票代码如 “000002”dateStata日期格式交易日格式为 %tdCCYY-NN-DDstatusstring当日状态取值为“正常”, “ST”, “*ST”。历史上极少的“PT”状态单独标注。is_stint虚拟变量当status为 “ST” 时等于1否则为0。便于直接筛选。is_sstint虚拟变量当status为 “*ST” 时等于1否则为0。便于直接筛选。4.1 在实证研究中的应用假设你正在研究“风险警示公告的市场反应”或“ST股票的投资价值”。* 示例1在事件研究中精确界定事件日 use your_event_study_data.dta, clear merge m:1 code date using st_status_panel.dta, keep(match) nogen * 确保在事件窗口期内你的样本股票没有发生ST状态变化避免混淆效应。 * 示例2在构建投资组合时排除ST/*ST股票 use your_stock_return_data.dta, clear merge m:1 code date using st_status_panel.dta, keep(match) nogen keep if is_st 0 is_sst 0 // 剔除所有风险警示股 * 这样就能确保你的因子收益率或策略回测不受这些特殊股票的影响。 * 示例3动态计算ST板块的指数 bysort date: egen st_mkt_cap total(mkt_cap) if is_st 1 | is_sst 1 bysort date: egen total_mkt_cap total(mkt_cap) gen st_ratio st_mkt_cap / total_mkt_cap line st_ratio date // 可以画出ST板块市值占比随时间变化的趋势图。4.2 在量化策略中的集成在回测框架中你需要在每个再平衡日例如每月底调用这份数据。* 假设你的回测周期是2005-01-01到2023-12-31月度调仓 forvalues y 2005/2023 { forvalues m 1/12 { local rebalance_date td(01m‘y‘) // 每月第一天 * 获取上个月末的股票状态 use st_status_panel.dta if date rebalance_date‘ - 1, clear * 将状态信息 merge 到你的候选股票池 merge 1:m code using candidate_stocks_y‘_m‘.dta keep if _merge 3 drop if is_st 1 | is_sst 1 // 剔除有风险警示的股票 * ... 后续进行你的选股逻辑 } }关键技巧在回测中一定要使用“前瞻未来”数据。即在t日做决策时只能使用t-1日及之前的信息。因此在合并状态数据时务必确保状态日期不晚于决策日期。我的数据集中date是交易日直接使用merge并确保日期对齐即可避免未来函数。5. 常见问题与维护更新指南5.1 数据更新2024年及以后这份数据更新到2023年底。如果你想获得2024年的数据或者未来持续更新你需要获取新公告定期如每周从沪深交易所官网爬取或下载新的风险警示相关公告。运行解析代码将新公告文本放入原始数据文件夹运行我提供的文本解析模块模块一。合并与重跑将解析出的新事件记录追加到老的event_dt.dta文件中然后重新运行从模块二开始的全套流程。我提供的代码是模块化的你只需要关注第一步获取新原料后面的清洗、匹配、填充、校验流程是完全自动化的。5.2 处理历史遗留问题与特殊案例A股的风险警示规则历史上经历过多次调整在数据处理中会遇到一些“坑”PT制度2002年前后存在“特别转让服务”PT这部分股票在特定日期的状态我单独标记为“PT”但它们在大多数现代分析中可被视为准退市状态可与*ST同等对待或单独处理。新旧ST规则早期的ST规则与现在有所不同比如对连续亏损年限的要求。但我们的数据只记录“是否被实施”这一事实不涉及原因因此不受规则细节变化的影响。B股、退市整理期股票数据集包含A股和B股。对于进入退市整理期的股票其状态在整理期首日即变更为“退市整理”这部分数据也已包含在内便于你区分。同一公司A/B股不同步极少数情况下一家公司的A股和B股可能被不同步实施ST。我的数据精确到每一只证券代码因此能准确反映。5.3 性能优化与大数据处理如果你的研究涉及全市场、全历史的高频数据直接操作日度面板可能会比较慢。有两个优化建议使用range和joinby替代cross在生成全量面板时对于股票数量多、时间跨度长的情况cross命令可能内存消耗巨大。可以使用range生成日期序列再与股票列表进行joinby效率更高。状态填充算法优化模块三中的循环如果编写不当在超大面板上会非常慢。一个高效的技巧是利用by code (date)分组后结合gen new_status status_to if effect_date date和replace current_status new_status if new_status ! .的方式然后使用carryforward命令或fillin配合ipolate来向前填充状态这比显式循环快几个数量级。这份数据和代码是我自己多年研究工作中沉淀下来的实用工具。它不华丽但足够扎实。金融数据分析很多时候功夫就下在这些基础数据质量的打磨上。希望它也能帮你扫清一些障碍让你更专注于策略和模型本身。如果在使用过程中有任何问题或者发现了数据中的任何疑点欢迎交流探讨。记住对数据永远保持一份怀疑和校验的心态是做好研究的第一课。