多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

沪深股票日线数据导入MySQL:MACD与CCI指标验证及量化回测避坑指南

沪深股票日线数据导入MySQL:MACD与CCI指标验证及量化回测避坑指南 简介这份数据集覆盖沪深股市自历史早期至2022年1月10日的全部日线行情面向量化研究员、技术分析者以及策略回测人员可解决历史数据获取与指标计算不便的问题。压缩包仅含一个SQL文件包体约424.52MB数据以结构化表格存储便于通过SQL语句按股票、日期或价格区间灵活检索、聚合并可与其他行情数据合并分析。已有611人浏览学习。记录包含开盘价、收盘价、最高价、最低价、振幅、成交量、成交额、换手率等基础字段并预计算了MACD、CCI等常用技术指标可直接用于识别趋势信号、判断超买超卖区间。分析师可基于这些数据开展因子验证、策略回测或构建预测模型也可留言请求扩展同花顺多空指标等个性化字段为不同层次的投研工作提供支持。1. 拿到这份沪深日线数据先别急着写策略做股票数据分析的人最痛苦的事不是算法不会写而是数据不干净、不连续、字段对不上。这份「沪深股票历史以来到2022-01-10的全部日线数据」解压后是一个 ts_data.sql 文件本质上是把沪深两市从早期到 2022 年 1 月 10 日为止的日线行情按关系型数据库的格式打包好了。对做量化回测、技术指标研究、或者只是想省去爬虫和时间对齐工作的人来说这份数据直接把最耗时的那一步——采集、清洗、入库——变成了导入即用。适合的人群很具体想用 MySQL 或同类数据库做股票数据分析和策略验证的开发者、想拿真实历史数据练手 MACD 和 CCI 指标实现的初学者、以及需要一份统一格式日线数据做内部工具原型的研究人员。但这份数据不是拿来就能跑出理想结果的字段口径有几个坑下面一步步说清楚。2. 把 ts_data.sql 跑进 MySQL从建库到验证数据完整性2.1 先搞清楚这份 SQL 文件到底存了什么拿到 ts_data.rar 解压后里面是单个 ts_data.sql 文件。不要急着直接source先看一眼文件头部结构。用文本编辑器打开前 100 行或者用head命令快速查看head -100 ts_data.sql重点看三件事建表语句的字段名、字段类型、以及 INSERT INTO 的表名。常见做法是这张表叫stock_daily之类的名字字段包含股票代码、交易日期、开盘价、收盘价、最高价、最低价、成交量、成交额、换手率、振幅以及现成的 MACD、CCI、多空指标等字段。确认字段名之后再决定后续查询用哪个字段名避免自己臆测。这里有个关键点这份数据既然已经算好了 MACD 和 CCI说明表里存的不是纯 OHLCV 原始行情而是带技术指标的结果集。这种设计的优点是导入后立刻能做筛选分析比如直接查出某只股票 CCI 超卖区间是哪几天。缺点是如果你自己算 MACD 和 CCI参数和口径必须和文件里一致否则对不上账。后面第四章我会专门讲如何验证这个一致性。2.2 导入实操用 mysql 命令导入的完整流程导入前先建一个目标数据库避免直接丢进默认库。按下面步骤走mysql -u root -p -e CREATE DATABASE IF NOT EXISTS stock_db DEFAULT CHARSET utf8mb4; mysql -u root -p stock_db ts_data.sql第一条命令创建一个名为stock_db的数据库字符集用utf8mb4目的是兼容中文表注释和可能出现的中文字段值。第二条命令把整个 SQL 文件导入 stock_db。导入时间取决于文件大小和机器性能。如果是几个 GB 的 SQL 文件机械硬盘可能要十几分钟SSD 会快很多。导入期间终端看起来像卡住是正常的不要直接 CtrlC。建议用mysql -u root -p stock_db --show-warnings -e SELECT COUNT(*) FROM stock_daily;这类方式验证行数。参数说明是 shell 重定向把文件内容作为 mysql 客户端的输入--show-warnings可以显示导入过程中的警告信息如果有字段截断或类型转换问题这里会暴露出来。如果导入中途报错先定位到报错行附近看是字段长度不够还是字符集问题。2.3 导入后快速验证数据完整性数据导进去不代表没问题。我一般会跑下面四条检查 SQL确认数据是不是真的能用-- 检查表是否存在以及总行数 SELECT COUNT(*) AS total_rows FROM stock_daily; -- 检查日期范围是否覆盖到 2022-01-10 SELECT MIN(trade_date), MAX(trade_date) FROM stock_daily; -- 检查股票数量大约多少只 SELECT COUNT(DISTINCT stock_code) AS stock_count FROM stock_daily; -- 检查关键字段是否有 NULL 值 SELECT COUNT(*) AS null_close_rows FROM stock_daily WHERE close_price IS NULL;逻辑说明第一条是数据量底线判断如果行数少得离谱可能是导入中断或文件本身不完整。第二条日期范围用来验证是否真的如标题所述覆盖到 2022-01-10。第三条看股票数量级沪深两市正常有几千只如果只有几百说明数据可能只包含部分股票。第四条检查收盘价空值空值过多会直接影响后续指标计算和策略回测。参数说明COUNT(DISTINCT stock_code)对大数据量表可能较慢如果查询超时可以改成先查索引再跑。WHERE close_price IS NULL是判断字段是否为空的写法不要用 NULL这是 SQL 里最常踩的坑之一。3. 理解字段语义这些指标不是你想象的那样3.1 换手率、振幅和成交额的口径差异很多人拿到数据直接用从来不问字段是怎么算出来的。换手率在文件里可能是百分比值如 1.25 表示 1.25%也可能是小数0.0125这两种差 100 倍回测策略的阈值设置就会完全不一样。判断方法很简单找一只成交活跃的股票比如某只日成交额几十亿的票看它的换手率字段是 5 还是 0.05就知道单位了。振幅同理可能是(最高价-最低价)/昨收的百分比也可能是直接最高价-最低价的绝对值。如果是后者不同价位的股票之间振幅无法横向比较。成交额也有两种常见口径以元为单位或以万元为单位。这些字段的语义不确定性是后续所有计算和策略判断的隐形地雷。要确认这些有一个笨但可靠的办法用 SQL 按一个已知交易日去对比公开行情页面数据。比如查某只股票 2021 年某天的开盘价、收盘价、成交量、成交额和换手率和网上行情快照做一次抽样比对比对 5 到 10 个样本就够看出口径了。3.2 数据清洗缺失值和停牌日期的处理日线数据里最影响分析质量的是停牌导致的缺失交易日。A 股股票停牌期间数据文件里通常没有那一行记录这会导致直接用该股票的时间序列计算指标时周期出现跳跃。常见处理方式是保留真实交易日期序列而不是按自然日填充。举个例子-- 查找某只股票疑似停牌导致的时间断档 SELECT a.trade_date AS prev_date, b.trade_date AS next_date, DATEDIFF(b.trade_date, a.trade_date) AS gap_days FROM stock_daily a JOIN stock_daily b ON b.stock_code a.stock_code WHERE a.stock_code 600000 AND b.trade_date a.trade_date AND b.trade_date (SELECT MIN(trade_date) FROM stock_daily WHERE stock_code a.stock_code AND trade_date a.trade_date) AND DATEDIFF(b.trade_date, a.trade_date) 10;逻辑说明这条 SQL 对指定股票逐行找到下一个交易日并计算间隔天数。间隔超过 10 天自然日的大概率是长假或者长期停牌。把这些日期段找出来后做指标计算时就知道哪些地方不能按连续交易日看待。参数说明DATEDIFF是 MySQL 里计算两个日期相差天数的函数b.trade_date a.trade_date配合子查询用来取下一个交易日的记录。如果数据库里有专门的交易日期表这个查询可以写得更优雅但直接靠表内自关联也能达到效果。gap_days 10这个阈值你可以自己调想找所有断档就把阈值设为 3 或 4。4. 自己重算 MACD 与 CCI验证数据的可信度4.1 用 Python 从原始价格数据重算 MACDMACD 的标准算法是先算 12 周期 EMA 和 26 周期 EMA两者相减得 DIF快线再对 DIF 做 9 周期 EMA 得到 DEA慢线MACD 柱状图是 DIF 与 DEA 之差乘以 2。这套算法在日线数据上几乎没有歧义所以很适合用来验证数据表里 MACD 字段是否可信。import pandas as pd def calc_macd(df, fast12, slow26, signal9): # df 需要按 trade_date 升序排列 df df.sort_values(trade_date) ema_fast df[close_price].ewm(spanfast, adjustFalse).mean() ema_slow df[close_price].ewm(spanslow, adjustFalse).mean() dif ema_fast - ema_slow dea dif.ewm(spansignal, adjustFalse).mean() macd (dif - dea) * 2 return dif, dea, macd逻辑说明ewm是 pandas 的指数加权移动平均实现span参数对应 MACD 的周期数adjustFalse表示用递归形式计算即 EMA(今日) alpha * 今日价格 (1 - alpha) * EMA(昨日)这是国内行情软件通用的 MACD 算法。用这个函数对某只股票全历史数据算一遍对比文件里自带的 MACD 字段是否一致。参数说明fast、slow、signal 三参数在多数行情软件里固定为12, 26, 9但你要验证的口径如果和这个不同算出来就会对不上。比对时建议随机抽 3 到 5 只股票每只对比最近 100 个交易日的数值允许浮点误差在 0.001 以内。如果误差很大说明文件里的 MACD 是用复权价还是不复权价计算的你需要再试。4.2 用同样方法验证 CCI 指标CCI 的标准计算方法是典型价格 TP (最高价 最低价 收盘价) / 3然后算 TP 的 N 周期简单移动平均再算平均绝对偏差最后 CCI (TP - MA) / (0.015 * 平均绝对偏差)。下面是 14 周期的完整代码import pandas as pd import numpy as np def calc_cci(df, n14): tp (df[high_price] df[low_price] df[close_price]) / 3.0 ma tp.rolling(windown).mean() md tp.rolling(windown).apply(lambda x: np.mean(np.abs(x - x.mean())), rawTrue) cci (tp - ma) / (0.015 * md) return cci逻辑说明rolling(windown).mean()是简单移动平均apply中的 lambda 计算的是平均绝对偏差而不是标准差这是 CCI 和别的摆动指标最大的区别。分母中的 0.015 是约定的常数目的是让 CCI 值在大部分时间落在正负 100 的区间内如果文件里的 CCI 数值范围和这个不一样要注意可能用了不同的常数。参数说明rawTrue可以提升计算速度n 通常取 14但不同软件也有用 20 的。这一步验证的意义在于如果文件里的 MACD 和 CCI 字段都能对上说明这份数据的其他指标比如多空指标大概率也是同一套体系算出来的可以放心用如果对不上你后续策略里就不要直接引用这些字段而是自己重算。4.3 字段值比对的具体操作方法把重算的结果和库里自带的字段做一次系统比较我一般在 pandas 里完成# 假设已经从数据库读出了 stock_daily 表到 df # df 需要包含 trade_date, close_price, macd_field, cci_field import pandas as pd sample_stocks [600000, 000001, 300750] for code in sample_stocks: tmp df[df[stock_code] code].copy() dif, dea, macd calc_macd(tmp) cci calc_cci(tmp) # 比较最后一行的差异 print(code, MACD diff:, abs(macd.iloc[-1] - tmp[macd_field].iloc[-1])) print(code, CCI diff:, abs(cci.iloc[-1] - tmp[cci_field].iloc[-1]))逻辑说明这个方法只对比每个股票最后一个交易日的差异优点是快速粗筛。如果差异接近 0说明算法口径一致如果差异巨大再往前多看几十个交易日确认是整体偏移还是个别数值跳变。有个坑如果库里存的是计算后的平移值比如指标值显示的是前一天的那么任何对齐方式都会差一天这种情况比较常见于部分国产行情软件的数据导出。5. 沪深日线数据使用避坑五个高频翻车现场5.1 除权除息导致的价格断层现象某只股票在分红送股后股价从 20 元突然变成 12 元日线图上出现一个巨大的向下跳空不像是市场行为。原因A 股除权除息日会对股价做强制调整这是交易所规则不是数据错误。如果直接拿未复权价格做时间序列分析MACD、均线、CCI 全都会在除权日出现虚假信号。解决做策略回测前先把价格序列做前复权或后复权处理。常见做法是拿到复权因子字段如果数据里有的话或者在导入后用 pandas 调用pct_change()基于涨跌幅重建价格序列。没有复权因子时可以用后复权法从最早日期开始将每日涨跌幅累乘得到的价格曲线在除权日不会有断层。5.2 停牌和交易日缺失让指标计算出现偏差现象某股票连续停牌 5 个月复牌后第一天 MACD 出现了极大的数值跳变与同行业股票走势完全不符。原因MACD 是递归算法EMA 值依赖前一天的 EMA。如果在数据里把停牌后的第一根 K 线和停牌前一天的 K 线当成连续两天中间缺失的 5 个月等于被无视算法当然会计算出极端值。解决在喂给 MACD 和 CCI 计算之前先把长期停牌区间的断点识别出来把股票按连续交易日分段处理。具体做法是先计算每个交易日与上一个交易日的间隔间隔时间超过阈值时在逻辑上断开对每段分别计算指标。这个阈值我一般设为 10 天但并不绝对节假日最长也就 7 到 8 天避开 10 天以上间隔足够干净。5.3 文件内指标字段的计算口径不确定现象同一只股票自己算出来的 CCI 与文件里的 CCI 在大部分交易日差一个常数偏移有时又完全一致。原因CCI 的计算有两种常见变体一种是分母使用平均绝对偏差另一种是使用标准差。用标准差算出来的 CCI 数值范围更小两条曲线形状相似但幅度不同。文件里如果用标准差版本直接套用平均绝对偏差版本会差很多。解决用前面第四章的重算方法先跑通比对确认口径之后把文件里的指标字段当作参考但不直接依赖。如果是做教学或原型验证建议统一用自己的算法重算一遍所有指标避免拿一个计算方式不透明的字段做决策依据。5.4 涨跌停判断和 ST 股的价格限制现象用(最高价 - 最低价) / 昨收计算振幅时某些 ST 股单日振幅只有 2%一些创业板新股却出现 40% 以上的振幅对比非常极端导致后续策略统计失真。原因ST 股涨跌停幅度是 5%创业板和科创板是 20%主板是 10%不同板块的涨跌停限制不同用同一套振幅阈值过滤所有股票会出现系统性偏差。解决在做全市场统计分析时按股票代码前缀或板块字段分组对每个组单独设置振幅阈值。具体判断时先看代码前缀60、00 开头是主板30 开头是创业板68 开头是科创板带 ST 标识的再降级。这些信息如果数据表里没有板就需要引入额外的股票基础信息表去关联。5.5 退市股票和特别处理股票的存活偏差现象用 2022 年之前的数据做回测筛选出的全是后来表现很好的股票所以策略收益率高得惊人实盘却达不到这个效果。原因这就是典型的存活偏差。数据文件中可能包含了后来退市的股票也可能文件生成时把退市股剔除掉了。如果只保留当前还在交易的股票历史回测会天然偏向那些没退市且历史走势较好的股票任何策略都会被高估。解决先确认表里是否包含退市股用股票代码范围排查是可行的但不够精确。更直接的办法是统计每年股票数量变化如果发现某年股票数量单调下降说明数据可能做过剔除处理。处理方式是只做个股策略研究不做全市场选股统计或者尽量拿到退市股名单后在分析中加入标记。数据文件本身没有退市标记时要接受这个局限。6. 把这份数据用到位构建一个 MACD 与 CCI 共振的日线策略6.1 策略逻辑与代码实现当这份数据的字段口径确认无误后可以拿来做实际策略原型。最典型也是新手最容易上手的做法是 MACD 和 CCI 共振MACD 的 DIF 线在零轴上方且从下向上穿过 DEA 线同时 CCI 从负值区域向上穿越 -100 线此时发出买入信号反向条件出现时发出卖出信号。用 pandas 实现核心部分def generate_signals(df, macd_fast12, macd_slow26, macd_signal9, cci_n14): # 数据清洗按日期排序后重置索引 df df.sort_values(trade_date).reset_index(dropTrue) # 计算 MACD 三线 ema_fast df[close_price].ewm(spanmacd_fast, adjustFalse).mean() ema_slow df[close_price].ewm(spanmacd_slow, adjustFalse).mean() dif ema_fast - ema_slow dea dif.ewm(spanmacd_signal, adjustFalse).mean() macd_hist (dif - dea) * 2 # 计算 CCI tp (df[high_price] df[low_price] df[close_price]) / 3.0 cci_ma tp.rolling(windowcci_n).mean() cci_md tp.rolling(windowcci_n).apply(lambda x: np.mean(np.abs(x - x.mean())), rawTrue) cci (tp - cci_ma) / (0.015 * cci_md) # 生成金叉和死叉信号 dif_prev dif.shift(1) dea_prev dea.shift(1) cci_prev cci.shift(1) buy_cond (dif dea) (dif_prev dea_prev) (cci -100) (cci_prev -100) sell_cond (dif dea) (dif_prev dea_prev) (cci 100) (cci_prev 100) return buy_cond, sell_cond逻辑说明shift(1)是为了取前一天的数值用于判断交叉行为而不是直接判断谁大谁小。买入条件是 DIF 上穿 DEA 且 CCI 同时上穿 -100这个组合试图捕捉的是趋势刚启动且动量从弱转强的时刻。卖出条件反过来DIF 下穿 DEA 且 CCI 下穿 100。参数说明ewm(span...)的 span 等价于 MACD 参数CCI 的rolling窗口为 14。实际使用时阈值 -100 和 100 是 CCI 的经典超买超卖分界线但你可以根据自己的验证结果调整为 -50 和 150。做回测时注意涨停日无法买入所以信号产生后需要判断次日是否在涨跌停限制内。6.2 回测验证时先做的一步过滤信号生成后别急着算收益。先把停牌、一字涨停、一字跌停的日子过滤掉否则回测结果会有很多无法成交的假信号。一个简单过滤函数def filter_tradable(df, buy_cond): # 一字涨停或跌停时价格涨跌幅超过 9.5% 则无法买入/卖出 prev_close df[close_price].shift(1) pct_change df[close_price] / prev_close - 1.0 tradable (pct_change.abs() 0.095) | prev_close.isna() return buy_cond tradable.fillna(False)逻辑说明A 股主板涨跌停是 10%但实际涨停价是四舍五入到分9.95% 也可能涨停封死。用 9.5% 做阈值是为了留安全边际避免把没封死的票也过滤掉。prev_close.isna()是处理每个股票的第一条数据因为没有前收盘价不该被过滤掉。参数说明fillna(False)会把 NaN 值统一当成不可交易避免出现因缺失数据导致的假信号。如果你用的是创业板或科创板数据阈值要相应改成 19.5% 或 19% 附近。这是我能从这份数据里真正榨出价值的方式先验证字段可信度再按自己的口径重算指标最后设置带有可执行性过滤条件的策略。这些年我每次拿到一份新的历史数据都会强制走一遍这套流程抽样本、对字段、查断点、复算指标、过滤不可交易日期。过程是机械的但正是这五步帮我提前躲开了无数次回测曲线好看实盘却一塌糊涂的局面。希望这套数据验证的思路也能让你拿到文件后少走几步弯路。本文还有配套的精品资源点击获取
返回列表