我叫赵晓军,今天想认真聊聊我一个折腾了一年多的个人项目——股票数据分析平台。做这个东西的初衷特别朴素:市面上的行情软件、App、网页版工具,能看K线、能画线、能算指标,但一旦我想把几个指标组合在一起做对比,或者想把某只票过去五年每一根K线背后的财务数据、波动特征拉出来做统计,立刻就会碰到各种边界。平台要么不给历史数据,要么接口不开放,要么图表超级卡,更别提自己写公式做量化因子了。与其将就着用,我决定自己动手做一个能跑完全流程的分析平台,从数据采集、清洗、指标计算,到可视化看板,全部自己掌控。这篇文章就把我在这个项目里踩过的坑、做出的关键选型、以及一些可以复用的经验完整分享一下,给想自己搭分析系统、做量化研究或者单纯想把数据玩明白的朋友一个参考。
我自己不是金融科班出身,写代码算是主业,对股票分析纯粹是兴趣。所以这个项目里没有什么高深模型,更多是数据处理、工程落地、以及如何让分析结果可解释、可复现的经验。平台本身是开放的,只做数据分析和统计展示,不做任何买卖点预测,也不构成投资建议。下面的内容,全是工程和技术视角的实战记录。
1. 项目缘起:我为什么非要自己造一个股票数据分析平台
1.1 现有工具解决不了什么
先说需求。我的日常工作流里,最常做的一件事是复盘:某只股票在某段时间涨了很多,我想知道它上涨前的基本面、量能、换手率、波动率是怎样的;或者某个行业指数走了一波行情,我想把成分股按相关性聚一下类,看看谁先启动。这种需求用传统行情软件做非常麻烦。它们擅长的是把现成的指标呈现在屏幕上,但很难让我批量处理几百只股票的三年日线数据,更难把行情数据和基本面数据拉到同一个日期对齐的DataFrame里做归一化分析。
另一个我特别在意的痛点是数据所有权。免费软件的数据是画在屏幕上的,你想导出,要么截屏,要么手动复制,特别原始。付费软件虽然提供数据接口,但往往限制调用频率和字段数量,而且很多时候我想用的是分钟线、复权因子、财报日历这类细节数据,接口不开放你怎么都拿不到。既然解决不了,干脆自己建一个数据仓库,把日线、月线、复权因子、交易状态这些基础数据都存到自己服务器上,想怎么用就怎么用。
1.2 平台定位:分析师的工作台,不是荐股工具
在动手之前,我给自己定了一条边界:这个平台是做“数据分析”而不是“自动交易”。它可以告诉你有多少人关注的指标现在处于什么状态,但不会告诉你“买入”“卖出”这种结论。因为一旦牵涉到交易决策,就需要更严苛的回测体系、滑点模型、资金管理,这就不是一个人业余时间能搞定的事情了。所以我的定位很清晰:一个辅助自己理解市场结构的数据库前端,一个研究股票之间相关性、因子有效性的实验台。
这条边界帮了我大忙。它让我可以用相对简单的技术栈完成闭环,同时不需要处理实盘交易对接、券商接口、风控这类非常容易被合规问题缠上的模块。整个平台就是一个“数据仓库 + 计算引擎 + 可视化看板”的组合,干净、可控、可拓展。
2. 数据层选型:从免费数据源到自建存储
数据是地基,这个环节我花了最多时间。刚开始我想着随便找几个接口,拉下来存CSV就行,但跑了两周就后悔了。数据源不稳定、字段口径不一致、停牌日缺失、复权因子不统一,问题层出不穷。后来我彻底推倒重来,把数据层分成了两个部分:上游数据获取和下游本地存储。
2.1 数据源对比与取舍
市面上的A股数据获取方式,我接触下来主要有几类:付费金融终端、开源社区维护的Python接口、以及各财经网站爬虫。付费终端数据质量最好,但个人购买价格偏高,而且很多接口限制终端环境。开源社区接口比如akshare、tushare,胜在免费且字段覆盖比较全,日线数据基本够用,缺点是对网络依赖较高,接口偶尔会变。爬虫的方式风险更高,页面改版就要重写逻辑,所以我只把它当作补充手段,用来获取一些非标准化的数据,比如部分公告摘要。
我最终选了开源Python接口作为主力数据源,并且做了归一化封装。什么叫归一化?就是不管上游返回什么格式,我在自己的数据接口层统一转成“code、date、open、high、low、close、volume、amount、adjust_flag”这样一个标准schema,再往下游存。这样即使哪天上游数据源挂了,或者我想从A源切到B源,只改动一个适配器就行,其余代码完全不用碰。
为了对比方便,我把当时调研的几个主要数据源列个表:
| 数据源 | 日线覆盖率 | 历史长度 | 分钟线 | 稳定性 | 成本 |
|---|---|---|---|---|---|
| 开源Python接口(如akshare) | 高 | 大部分有上市以来日线 | 部分有 | 一般 | 免费 |
| 社区版Tushare | 高 | 需要积分 | 部分有 | 较好 | 注册即可 |
| 付费终端API | 很高 | 完整 | 完整 | 好 | 高 |
| 自写爬虫 | 看页面 | 可定制 | 难 | 低 | 无但维护成本高 |
最终我选择“开源接口为主、自研爬虫为辅”的组合,主要原因就是成本可控和字段灵活。如果你只想做个人研究,这个组合完全够用。
2.2 存储选型:从SQLite到ClickHouse
最早我图省事,直接把数据存在SQLite里。数据量几千只股票、每只几千行,SQLite也能跑。但后来我开始加入分钟线、逐笔成交,数据量一下从几十万行到了几亿行,SQLite的查询明显吃力,尤其是跨多只股票做时间对齐、计算累积收益率时,随便一条SQL都要等几十秒。这时候我换了ClickHouse。
为什么选ClickHouse而不是MySQL或PostgreSQL?核心原因是我的查询模式是典型的大范围扫描加聚合:比如“计算全市场所有股票在最近250个交易日的平均真实波幅”,或者“统计某一天全市场涨跌分布”,这类查询涉及几百万行数据聚合。ClickHouse的列式存储和向量化执行对这种查询简直是为我量身定做的。它的建表语句也很简洁,日线表和分钟线表就是典型的分布式时间序列表。
我当时的表结构大概是这样一个设计:
CREATE TABLE daily_kline ( code String, trade_date Date, open Float64, high Float64, low Float64, close Float64, volume UInt64, amount Float64, adjust_type String ) ENGINE = MergeTree PARTITION BY toYYYYMM(trade_date) ORDER BY (code, trade_date)分区按照月份来,按股票代码和时间排序,这样查询单只股票历史K线时能快速定位到对应分区,聚合全市场某一天数据时也能在列上做高效扫描。使用ClickHouse之后,之前需要几十秒的聚合查询大多在一秒内完成,甚至几百毫秒就能出结果,整个平台的手感立刻不一样了。
2.3 增量更新与补数机制
数据缓存和增量更新是这个项目里最容易“事倍功半”的地方。刚开始我是每天收盘后全量拉一遍所有股票的日线,即全部覆盖写入。数据量尚可,但接口调用次数多,经常触发限流,而且一旦网络中断,今天的数据就补不回来。后来我改成了“增量 + 补数”策略:
- 每个交易日收盘后,只抓取当天有更新的股票数据,按股票代码和时间戳增量合并;
- 每天开盘前做一个一致性检查,从库里找出最近一个交易日缺失的股票,重新拉取补数;
- 每周做一次全量复核,用交易日历和数据库中的最大交易日期做对账,确保没有断点。
这听起来很基础,但实际做起来会碰到很多细节。比如某些股票当天停牌,日线数据不会有更新,如果你简单地把“今天没有数据”当成“数据缺失”,就会不断重复请求,把自己IP搞封。正确做法是先拿到当天的停牌列表,把停牌股票排除在补数队列之外。另一件容易被忽略的事是交易日历。A股的交易日受节假日调休影响,不是简单的周一到周五,所以必须维护一张交易日历表,而不是用datetime库里的工作日函数去猜。
def get_missing_dates(begin_date, end_date): trade_dates = load_trade_calendar(begin_date, end_date) have_dates = set( db.query('SELECT DISTINCT trade_date FROM daily_kline WHERE code = ?', code) ) return [d for d in trade_dates if d not in have_dates]这个逻辑看起来简单,但补数的时候要特别注意区间边界。比如一只股票上市第一天是2015年6月1日,你不能在它上市前去补数据,所以每只股票我都维护了一个listing_date字段,和交易日历取交集之后再发起补齐。
3. 指标计算:从技术指标到自定义因子
数据就位之后,重头戏是计算层。这一层我踩的坑最深,很多坑不是技术上的,而是金融数据处理细节上的。如果你直接用pandas里的rolling(20).mean()算20日均线,看起来没错,但一旦涉及复权、停牌、涨跌停,结果可能和你看到的行情软件完全不同。
3.1 复权处理:很多人在这里翻车
先说复权。股价因为分红送股会有一个跳空缺口。比如一只股票10送10,除权日股价从30元变成15元,如果你直接用原始价格算收益率,会得到“一天暴跌50%”的荒谬结论。所以必须用复权因子把前后价格调整到一个可比的时间轴上。
复权分为前复权和后复权。前复权是以当前价格为基础,把历史价格调低;后复权是以历史价格为基准,把当前价格调高。做历史收益率统计时,建议用后复权,因为它不会随着时间推移不断改动历史价格。计算平台里,我存的是原始价和复权因子两个字段,需要前复权还是后复权就在计算时动态转换,而不是把转换结果直接写回原始表。这样做的好处是数据可追溯,避免反复写表导致的数据口径漂移。
具体到复权因子的使用,很多开源数据源会返回一个adjust_factor字段。它的计算逻辑不是简单的乘法,因为分红送股除权都影响价格,但每股净资产和公积金转增的处理方式不同。我当时的做法是把复权因子当成一个累积的乘数,从上市首日一直乘到最新一天。处理复权数据的核心结论就是:在算一切和收益率有关指标之前,先在时间轴上做后复权,算完指标再映射回当前价,否则所有基于历史价的历史指标都是失真的。
3.2 技术指标的计算陷阱
技术指标看起来简单,实际上到处是细节。以MACD为例,它的经典算法是:
ema_fast = close.ewm(span=12, adjust=False).mean() ema_slow = close.ewm(span=26, adjust=False).mean() dif = ema_fast - ema_slow dea = dif.ewm(span=9, adjust=False).mean() macd = 2 * (dif - dea)很多教程都是这么写的,但实际用起来有几个坑。第一个坑是EMA的初始值。经典的MACD公式里,初始EMA取的是第一个价格,但如果你用pandas的ewm方法不仔细看,初始值会受到adjust参数的影响。adjust=False代表从第一个数据点开始递归计算,而adjust=True代表使用EWM的加权平均方式进行修正,两种方式在数据较短时结果差异明显。我坚持用adjust=False,因为这才是交易软件里常见K线图上的MACD算法。
第二个坑是NaN值。某只股票停牌一天,K线数据里缺了那一天,直接对含有NaN的序列算EMA,ewm会把NaN视为“悬空”,不会把权重分摊到缺失点。造成的结果是均线比实际情况滞后。我的解决办法是在做指标计算前,把缺失日期补成NaN,再使用统一的fillna(method='ffill')策略,让停牌日的收盘价等于停牌前最后一天,这样均线才是你要的样子。
第三个坑是自然周和交易日。像RSI、KDJ这类周期指标,窗口是基于K线根数的,不是自然日。跨过长假的时候,如果不小心用了自然日窗口,指标会莫名其妙地突变。所以所有窗口计算都应该用“K线数量”而不是日历天数。
3.3 自定义因子:让平台真正变成研究工具
技术指标只是基础,我更期待的是能把自己的想法写成因子跑回测。比如我想研究“波动率因子”:过去20天的平均真实波幅(ATR)在全市场的横截面分布是怎样的;或者我想看看“放量突破”的定义用什么样参数更稳定。这时候平台就需要提供一个因子注册机制。
我设计了一个简单的接口,每个因子就是一个函数,输入是某只股票的历史行情DataFrame,输出是一个Series或一个标量。平台会把这些因子结果缓存成一张宽表,方便后续和收益率做相关性分析。
def atr_factor(df, period=14): high, low, close = df['high'], df['low'], df['close'] tr1 = high - low tr2 = (high - close.shift(1)).abs() tr3 = (low - close.shift(1)).abs() tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) return tr.rolling(period).mean() register_factor('atr14', atr_factor, '波动率因子-真实波幅14日')注册机制最大的好处是让分析流程标准化。同一个因子可以对全市场所有股票批量计算,算完之后直接入库。这样就不用每次想验证一个想法时都临时写一堆脚本。而且因子缓存之后,配合ClickHouse的大宽表查询,筛选“近一年波动率排名前10%的股票”这种操作就是一条SQL的事。
这里我想强调一下因子计算时的内存和性能问题。全市场5000多只股票,每只都要算几十个因子,如果全部用Python循环,速度会非常慢。我后来用了两层优化:第一层是因子计算支持groupby.apply的向量化写法,尽量避免逐行循环;第二层是把高频因子计算放到ClickHouse的SQL里做,比如简单的滚动均值、标准差,用windowFunnel或自建UDF实现,Python只负责生成参数和接收结果。
4. 可视化与交互:让分析结果真正可用
数据仓库里有了数据,计算引擎能批量出因子,但是最后还是要有人去看、去用。我一开始只做了一张简单的HTML表格,发现根本坚持不下来。后来花了大力气做了交互看板,整个平台才算活起来。
4.1 选型:为什么是React + ECharts
可视化技术栈是前端选型的老问题。我考虑过直接用Jupyter Notebook,也可以用Grafana。Notebook适合研究探索,但要嵌到一个统一平台里,交互体验太弱;Grafana监控时序很强,但股票分析需要复杂的自定义图表,比如K线叠加成交量、用画笔标注关键位置,Grafana做起来很费劲。
最后我选了一个自己熟悉的前后端分离方案:前端用React + ECharts,后端用Python FastAPI提供JSON接口,数据库直接连ClickHouse。React负责页面路由和状态管理,ECharts处理各类图表渲染。为什么不用Ant Design Charts或者Highcharts?因为我需要ECharts这种高度自由的配置,可以自定义K线颜色、缩放事件、联动tooltip,比较适合做研究型的图表。
另外ECharts的K线图组合需求是现成的,比如two y-axis显示价格和成交量,dataZoom做缩放,这些基础能力都有。我只需要把数据从ClickHouse拼装成ECharts需要的[date, open, close, low, high]格式,前端画起来非常顺。
4.2 看板布局:从全局到个股的下钻路径
这个平台的看板我按照“宏观 → 中观 → 微观”三层设计:
- 第一层是全市场概览:显示当日涨跌分布、成交量分布、近期新高新低家数,以及主要指数的热力图。
- 第二层是个股筛选器:通过自定义因子组合筛选,比如“近20日涨幅 > 10%且波动率小于某个阈值”,筛完进入列表。
- 第三层是个股详情:展示目标股票的完整K线图、成交量、均线、MACD、ATR布林带,以及自定义因子的时间序列。
交互上最重要的一点是下钻路径要顺滑。从全市场热力图点击某个行业,可以自动筛选出该行业成分股;从成分股列表点击某只票,可以直接跳到该票的详情页,而且详情页会携带当前行业筛选的条件,方便对比板块内个股。
这套下钻逻辑看起来不难,但设计时需要考虑URL状态管理。我用了React Router的query参数把筛选条件全部写在URL里,这样每次点击都生成一个新URL,浏览器前进后退都能保留状态,非常便于整理分析思路。
4.3 大数据量渲染性能优化
前端渲染K线时,最怕一次性plot几千根柱子。ECharts渲染几万根K线会明显掉帧,而且tooltip的回调也会变得很卡。我的解决思路不是减少渲染量,而是分级下采样:
- 使用dataZoom时,视野范围内如果有超过3000根K线,就先用十根K线合并成一个箱体(最高、最低、开盘、收盘取代表性的值),做一个降采样版本;
- 当用户放大到3000根以内时,再切换到原始K线数据。
- K线图下方另挂一个迷你缩略图(ECharts的
dataZoom内嵌组件),用极低分辨率的面积图表达大趋势,用户拖拽缩放时不会重新请求数据,而是从缓存中动态调整主图的范围。
前端性能优化还有一个容易被忽视的点:接口返回的JSON字段别带太多冗余字段。我最初给详情页返回了全字段,包括amount、turnover_rate等不参与渲染的列,导致传输体量大。后来我把K线返回精简为[date, open, close, low, high, volume]六个值,前端解析速度直接翻了倍。
5. 实测中的意外情况与应对
平台搭建过程中,最磨人的不是架构设计,而是各种真实数据中冒出来的意外情况。这些坑如果你自己不动手做一遍,光看文档是永远意识不到的。
5.1 除权日曲线断崖的真相
第一次看到自己平台上的K线图,我差点以为是数据错了。某只股票的历史日线在某个节点突然跳空低开,后面整个曲线都往下平移了一段,完全看不到真实上涨趋势。我检查了数据源,发现这就是原始未复权的数据。
我的处理办法是把复权因子在入库时就计算好并存储。每次计算收益率时,都用后复权价做运算;但显示K线时,默认用前复权价,因为大部分人习惯看前复权的图。这里有个很关键的细节:后复权价会随着时间推移不断变大,不利于显示近期K线的绝对坐标,所以前端展示前复权更有优势。而复权因子的计算必须尽量准确,否则整个时间序列的收益率统计都会出错。
5.2 停牌和涨跌停对数据计算的影响
平台上线后,我碰到一个非常奇怪的现象:某只股票连续三个涨停,但我算出来的20日累计涨幅和其他软件对不上。排查后发现问题出在涨停当天的开盘价处理上。很多股票涨停打开后的开盘价并不是直接从昨收价格上来,而是高开后快速封板。由于我的计划是用收盘价算收益率,但若只取收盘价,涨停那几天数据看起来从10元到11元再到12.1元,好像正常。但如果这只股票在涨停前停牌了,复牌首日没有涨停幅度的限制计算基准,它的“昨收”和“前收”会有差别,这一点必须从历史数据里读取真实的prev_close字段。
更隐蔽的是长期停牌后的复牌价格处理。如果某股票停牌六个月,复牌当天价格直接翻倍,你如果拿复牌前一天的收盘价作为基准计算收益率,会得到一个异常巨大的单日收益率。从市场真实交易角度看,这种收益并不是同一天实现的,应该把它标记为停复牌事件,在统计因子分布时单独处理或剔除。否则全市场异常值捡出来,因子分析的结果会被严重扭曲。
5.3 API限流与数据缺失的自愈
前面提到数据源接口会限流,尤其是免费接口,一小时内请求次数过多会直接封IP。我一开始的做法是重试N次,但效果很差,因为封禁是短时的,重试只会让封禁时间更长。后来我设计了一个简单的限流队列:
- 数据抓取任务按股票代码分片,每批100只;
- 每批请求之间sleep一个随机时间,比如0.5到1.5秒,避免瞬时请求峰值;
- 如果碰到限流错误,把对应股票代码放进重试队列,等5分钟后再试;
- 每天设置抓取时间窗口,如果当天无法完成全部更新,不硬刚,等第二天增量补偿。
这个策略帮我大大减少了接口被锁死的次数。同时我在库里对每只股票的更新时间做了记录,哪只票数据旧了,第二天早上自动跑一次单股票补数任务,不用人工干预。这套自愈机制让平台后续维护成本变得很低,基本只需要每个月看一次日志。
6. 平台的当前边界与下一步打算
6.1 目前不做的事:自动交易与荐股
虽然平台已经可以计算大量指标和因子,但我有意识地把“交易决策”相关功能全部封印。原因很简单:个人投资者直接对接实盘交易接口,涉及到券商合规、交易通道稳定性、模型过拟合等一系列问题,不是简单调用一下买入卖出接口就完事的。
我也从不在平台里输出“明天看多”“目标价”这一类预测性结论。我给自己的定位始终是:把数据整理好、把指标算清楚、把可能的机会和风险用数字呈现出来,最后决策还是人来做。这样既可以让平台保持研究工具的纯粹性,也能避免陷入不切实际的“自动赚钱”幻想。
6.2 下一阶段:轻量回测引擎与组合分析
目前平台最欠缺的是一个正式的回测引擎。我一直想做的是把一个自定义因子或策略模型拿到历史数据上验证,比如“当ATR高于某阈值且价格站上60日均线时持仓,直到跌破20日均线平仓”,这类规则需要独立的会话管理、持仓记录和绩效统计。
我的初步打算是用ClickHouse存干净K线数据,用Python做一个事件驱动的轻量回测引擎。回测引擎不追求毫秒级撮合,只做日线级别的模拟,重点处理滑点估计、停牌不可交易、涨跌停封单无法买入等约束。回测结果显示出来之后,再和平台的图表联动,展示每一笔交易的进入和退出点,方便判断策略是否真的在历史上有效。
另外一个方向是组合协同性分析。把行业成分股或概念板块拿出来,计算它们之间收益率的相关系数矩阵,然后在热力图上展示。这样能帮我快速理解一个板块里的股票是否足够分散、是否存在明显的同涨同跌。这个功能在传统软件里很难做,但在自己的平台上,只要把行情表转成透视表,用ClickHouse的corr函数就能高效跑出来。
折腾这个平台最大的体会是:数据分析工具的价值不在于功能多炫,而在于你能不能信任它给出的每一个数字。当你亲眼从原始数据到清洗逻辑,再到指标计算的每一步都验证过,你对市场的观察和判断才会有真正的底气和分寸。