1. 金融数据服务从零搭建的完整思路
1.1 为什么我要自己搭一套金融数据服务
先说清楚这个项目到底在干什么。financial-services这个名字听起来很泛,实际上它指的是一个面向个人开发者和小型团队的自建金融数据服务层——把行情数据、基本面数据、宏观经济指标的采集、清洗、存储和查询接口串成一条完整的链路,对外暴露统一的 API,供策略回测、看板展示、自动化报表等场景调用。
我做这个事情的起因很直接:市面上现成的金融数据接口要么贵得离谱,要么免费额度卡得死死的,要么字段残缺、更新延迟大到没法用。而自己从头写爬虫又面临一堆问题——数据源格式不统一、字段命名混乱、历史数据缺失、接口限流、断线重连、时区处理……每次开新项目都要把这些脏活重写一遍。所以我就想,干脆抽出一个独立的服务层,把所有这些杂事封装掉,上层业务只管调接口拿干净数据。
这套东西适合谁?如果你是一个人在做量化策略研究、写个人理财看板、跑一些宏观经济指标的自动跟踪,或者你是一个三五人的小团队需要一套内部共用的数据底座,那这套思路基本可以直接抄。它不追求高频交易那种微秒级延迟,也不涉及任何需要特殊资质的业务,纯粹是公开数据的采集、整理和分发。
核心关键词就一个:financial-services。我把它理解为一个服务化的金融数据中间层,而不是某个具体的交易系统或行情终端。
1.2 整体架构怎么拆
我把整个服务拆成了四层,从下往上依次是:
- 数据采集层:负责从各个公开数据源拉取原始数据,包括行情接口、公开报表页面、结构化数据文件等。
- 数据清洗与标准化层:把不同来源的数据统一成内部标准格式,处理缺失值、异常值、时区转换、复权计算等。
- 存储层:根据数据特性分别存入关系型数据库、时序数据库和对象存储。
- 服务接口层:对外提供 RESTful API 和 WebSocket 推送,封装鉴权、限流、缓存和日志。
这么拆的理由很简单:采集和清洗的逻辑变化最频繁(数据源经常改版),存储和接口相对稳定。分层之后,改采集不会影响接口,换存储不会影响业务调用。我见过太多人把所有逻辑塞在一个脚本里,数据源一改版整个系统就崩了,排查起来极其痛苦。
另一个关键决策是不追求实时性。日频数据用定时任务每天跑一次就够了,分钟级数据用轮询加增量拉取,只有真正需要实时推送的场景才上 WebSocket。这样做的代价是延迟从秒级变成分钟级,但换来了架构的极大简化和成本的急剧下降。对于绝大多数个人和小团队场景,这个取舍是划算的。
2. 数据采集层的核心细节与实操要点
2.1 数据源选型的几个硬标准
选数据源这件事,我踩过的坑比想象中多。总结下来有五个硬标准,缺一个都会在后面给你找麻烦:
- 字段完整性:至少要包含时间戳、开盘、最高、最低、收盘、成交量这六个基础字段。很多免费源缺最高最低,或者成交量单位不统一,用起来要额外补。
- 历史深度:日频数据至少要有五年以上的历史,否则回测样本不够,策略容易过拟合。
- 更新频率与延迟:日频数据要在收盘后两小时内更新完毕,分钟级数据延迟不能超过五分钟。
- 接口稳定性:连续跑一周不掉线、不返回空数据、不突然改字段名。
- 访问限制:明确知道每天的调用次数上限和频率限制,避免被封。
我一般会同时接两到三个源做交叉验证。比如主源用某个结构化接口,备源用另一个,每天跑完后对比收盘价,偏差超过千分之一就告警。这样能及时发现某个源出问题。
注意:不要把所有数据源都指向同一个上游。有些看起来不同的接口,底层其实是同一家数据商,一个挂了全挂。
2.2 采集任务的调度设计
采集任务我用的是分级调度策略,而不是一刀切:
| 任务级别 | 数据频率 | 调度方式 | 失败重试 |
|---|---|---|---|
| L1 实时 | 分钟级 | 常驻进程轮询 | 立即重试3次 |
| L2 日频 | 每日一次 | 定时任务 | 间隔5分钟重试5次 |
| L3 周频 | 每周一次 | 定时任务 | 间隔30分钟重试3次 |
| L4 月频 | 每月一次 | 手动触发+定时 | 间隔1小时重试2次 |
这么分的原因是不同的数据对时效性要求完全不同。分钟级行情断了要马上补,但月度宏观经济数据晚几个小时甚至一天都无所谓。如果全部用同一套重试逻辑,要么实时数据补得太慢,要么低频数据浪费大量重试资源。
调度器我用的是轻量级的方案,一个主进程加多个 worker,通过消息队列分发任务。每个任务执行完写一条状态记录到数据库,包含开始时间、结束时间、拉取条数、状态码。这样出问题的时候能快速定位是哪个环节卡住了。
2.3 限流与反爬的应对思路
公开数据源基本都有访问频率限制,硬闯只会被封。我的做法是:
- 令牌桶限流:每个数据源维护一个独立的令牌桶,按接口文档给的 QPS 上限设置速率,请求前先取令牌。
- 请求间隔随机化:在基础间隔上叠加一个随机抖动,比如基础间隔200毫秒,实际间隔在180到250毫秒之间随机。这样请求曲线更接近自然访问。
- 失败退避:连续失败时指数级增加等待时间,第一次失败等1秒,第二次等2秒,第三次等4秒,最多等到64秒。
- 多源轮换:同一个数据有多个源时,轮流使用,降低单源压力。
这里有个细节很多人忽略:User-Agent 和请求头要模拟正常浏览器行为。不是让你去伪造身份,而是很多接口对默认的编程语言请求头直接拒绝。设置一个常见的浏览器 UA,加上 Accept、Accept-Language 等标准头,通过率会高很多。
实操心得:我习惯在采集层加一个“健康度评分”,每个源根据最近一小时的请求成功率、平均延迟、数据完整度算一个分数。调度时优先用高分源,低分源自动降权。这个机制帮我省了大量手动切换源的时间。
3. 数据清洗与标准化层的实现细节
3.1 统一数据模型的字段设计
不同数据源返回的字段名千奇百怪,有的用open,有的用Open,有的用开盘价。清洗层的第一件事就是定义一套内部标准字段,所有数据进来先做字段映射。
我的标准行情模型包含以下字段:
symbol:标的代码,统一大写,去除交易所前缀后缀trade_date:交易日期,统一为 ISO 8601 格式trade_time:交易时间,分钟级数据才有open_price、high_price、low_price、close_price:四个价格字段,统一为浮点数volume:成交量,统一为股数turnover:成交额,统一为元adjust_flag:复权标志,0 不复权,1 前复权,2 后复权source:数据来源标识ingest_time:入库时间戳
字段映射用配置文件管理,每个数据源一个映射表。这样新增数据源只需要加一个配置,不用改代码。
3.2 缺失值与异常值的处理策略
金融数据里缺失值和异常值太常见了,处理不好会直接污染回测结果。我的策略分三步:
第一步,标记而非直接填充。发现缺失值时,先写一条记录到异常表,保留原始上下文,而不是直接填个0或者前值。因为有些缺失是真实的(比如停牌),有些是采集失败,处理方式完全不同。
第二步,按类型分别处理。停牌导致的缺失,价格字段用前收盘价填充,成交量填0;采集失败导致的缺失,触发重新采集;数据源本身就没有的字段,标记为 NULL 并在接口层说明。
第三步,异常值检测。我用的是简单的统计方法:计算过去20个交易日的收益率均值和标准差,如果某天收益率超过5倍标准差,标记为疑似异常。对于疑似异常,不直接删除,而是标记出来,人工确认后再决定。
注意:涨跌停导致的极端收益率是正常的,不要误判。检测时要结合涨跌停规则一起判断。
3.3 复权计算的原理与实现
复权是金融数据处理里绕不开的一环。不复权的价格在除权除息日会出现跳空,直接用于回测会严重失真。
前复权的逻辑是:以最新价格为基准,把历史价格按分红送股比例调整。后复权是以最早价格为基准,把后续价格调整。我一般用前复权,因为看盘时最新价格和实际一致,比较直观。
计算过程不复杂,但有几个坑:
- 除权除息日的判断要准确,不同市场的规则不一样。
- 分红和送股要分开处理,送股影响股数,分红影响现金。
- 复权因子要保留足够精度,否则长期复权后误差会累积。
我的做法是维护一张复权因子表,每次除权除息事件发生后更新。计算前复权价格时,用当前价格乘以复权因子。这样不用每次重新计算全历史,效率高很多。
4. 存储层选型与接口层设计
4.1 三类存储的职责划分
存储层我用了三种存储介质,各司其职:
- 关系型数据库:存标的元信息、交易日历、复权因子、采集任务状态等结构化程度高、数据量不大的数据。我用的是 PostgreSQL,主要是看中它的 JSON 字段支持和窗口函数。
- 时序数据库:存行情数据。日频数据量不大,用关系型也能扛,但分钟级数据一天就是几十万条,关系型写入和查询都会吃力。我选的时序库支持按时间分区和自动降采样,查询最近N天的数据很快。
- 对象存储:存原始数据快照和备份文件。每次采集的原始响应都存一份,方便出问题时回溯。
这么分的理由是:不同类型的数据访问模式完全不同。元信息读多写少,行情数据写多读也多,原始快照几乎只写不读。用同一种存储硬扛,要么成本高,要么性能差。
4.2 API 接口的设计原则
接口层是对外服务的门面,设计好坏直接影响使用体验。我遵循几个原则:
统一响应格式。所有接口返回同样的结构:
{ "code": 0, "message": "success", "data": { ... }, "request_id": "abc123" }code为0表示成功,非0表示各种错误。request_id用于追踪问题。
分页与批量查询。行情查询接口支持按标的列表批量查询,一次最多查50个标的。返回结果按时间和标的分组,避免调用方自己拼装。
缓存策略。日频数据当天不会变,缓存24小时;分钟级数据缓存5分钟;元信息缓存1小时。缓存用内存加 Redis 两级,减少数据库压力。
限流与鉴权。每个调用方分配一个 API Key,按 Key 限流。免费额度每分钟60次,付费额度可以调高。鉴权用简单的 Header 传 Key,不搞复杂的 OAuth。
4.3 接口性能优化的几个手段
接口响应慢是常见问题,我用了几个手段优化:
- 预计算:常用的技术指标(如均线、MACD)在数据入库时就算好存起来,查询时直接读,不用实时计算。
- 分区裁剪:时序数据按时间分区,查询时只扫描相关分区,避免全表扫描。
- 连接池:数据库连接用连接池管理,避免每次请求都新建连接。
- 异步日志:请求日志异步写入,不阻塞主流程。
实测下来,日频行情查询接口的 P99 延迟从最初的800毫秒降到了50毫秒以内,分钟级数据查询从3秒降到了200毫秒左右。
5. 常见问题与排查技巧实录
5.1 数据源突然改版怎么办
这是最常见也最头疼的问题。数据源改版通常表现为:字段名变了、返回结构变了、接口地址变了、或者直接返回错误码。
我的应对流程是:
- 监控告警:采集任务连续失败3次触发告警,第一时间知道出问题了。
- 快速定位:查看原始响应快照,对比历史响应,找出变化点。
- 临时降级:如果主源挂了,自动切换到备源,保证数据不断。
- 修复映射:更新字段映射配置,重新跑一遍采集。
- 数据补录:把改版期间缺失的数据补上。
实操心得:我习惯在每次采集时把原始响应存一份到对象存储,保留30天。这样出问题时能快速对比,不用去猜数据源改了什么。
5.2 时区问题导致的日期错乱
时区问题极其隐蔽,但影响很大。比如美股数据用美东时间,A股数据用北京时间,如果统一按 UTC 存储,查询时不做转换,就会出现日期对不上的情况。
我的处理原则是:存储统一用 UTC,接口层按调用方指定的时区返回。数据库里所有时间字段都是 UTC,接口增加一个timezone参数,默认返回 UTC,调用方可以指定Asia/Shanghai或America/New_York。
另外,交易日历也要按时区处理。不同市场的交易日不同,不能用一个统一的日历。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方法 |
|---|---|---|---|
| 采集任务全部失败 | 网络问题或源站宕机 | 检查网络连通性和源站状态 | 切换备源,等待恢复 |
| 部分标的无数据 | 标的代码格式不对 | 对比源站代码格式 | 更新代码映射 |
| 数据延迟大 | 调度任务堆积 | 查看任务队列长度 | 增加 worker 或优化任务 |
| 接口响应慢 | 缓存失效或查询未走索引 | 查看慢查询日志 | 加缓存或优化索引 |
| 复权价格不对 | 复权因子未更新 | 检查除权除息事件 | 更新复权因子表 |
| 数据重复 | 重试机制导致重复写入 | 检查唯一约束 | 加唯一索引或去重逻辑 |
5.4 几个我踩过的坑
坑一:用浮点数存价格导致精度丢失。金融价格对精度要求高,浮点数在多次计算后会出现误差。后来我改用定点数存储,价格统一乘以10000存整数,展示时再除回来。
坑二:批量插入时事务太大导致锁表。一次插入几十万条数据,事务太大,数据库锁表严重。后来改成每1000条一个批次,分批提交,问题解决。
坑三:忽略接口的幂等性。重试机制如果没有幂等保证,会导致数据重复。后来我在写入前先检查唯一键,存在则更新,不存在则插入。
坑四:日志打太多影响性能。调试阶段打了大量日志,上线后没关,导致磁盘IO成为瓶颈。后来改成分级日志,生产环境只打 WARN 以上级别。
6. 部署与运维的实战经验
6.1 容器化部署的注意事项
我用 Docker Compose 做本地部署,生产环境用轻量级容器编排。几个注意点:
- 数据持久化:数据库和对象存储的数据目录必须挂载到宿主机,容器重建不丢数据。
- 健康检查:每个服务配置健康检查接口,编排系统根据检查结果自动重启异常容器。
- 资源限制:给每个容器设置 CPU 和内存上限,避免一个服务拖垮整台机器。
- 日志收集:容器日志统一输出到标准输出,由日志驱动收集,不要写在容器内部。
6.2 监控与告警配置
监控我分了三个层面:
- 基础设施层:CPU、内存、磁盘、网络。用系统自带的监控工具加告警规则。
- 应用层:接口响应时间、错误率、采集任务成功率。用应用埋点加时序数据库。
- 业务层:数据更新延迟、数据完整度、异常数据比例。自定义指标加告警。
告警渠道我用的是邮件加即时通讯工具。告警规则要设置合理的阈值和静默期,避免告警风暴。比如采集失败告警,连续失败3次才触发,触发后30分钟内不重复告警。
6.3 数据备份与恢复策略
数据是核心资产,备份不能省。我的策略是:
- 每日全量备份:每天凌晨把数据库全量导出,存到对象存储,保留30天。
- 实时增量备份:数据库开启 WAL 归档,支持按时间点恢复。
- 异地备份:每周把全量备份同步到另一个区域的对象存储。
- 恢复演练:每季度做一次恢复演练,确保备份可用。
注意:备份文件要加密存储,访问权限严格控制。恢复演练一定要做,我见过太多人备份了但从来没恢复过,真出事时发现备份是坏的。
7. 后续可以扩展的方向
这套服务跑稳定之后,我陆续加了一些扩展功能,都是实际用起来觉得需要的:
技术指标计算服务。把常用的技术指标计算封装成独立接口,调用方传标的和时间范围,返回指标序列。这样不同项目不用重复实现。
数据质量报告。每天生成一份数据质量报告,包含各数据源的采集成功率、数据完整度、异常数据统计。发到邮箱,一眼就能看出有没有问题。
多市场支持。最初只支持A股,后来加了港股和美股。不同市场的交易日历、交易时间、复权规则都不一样,扩展时要特别注意。
回测数据快照。每次回测时把用到的数据打一个快照存起来,保证回测结果可复现。这个功能在策略迭代时特别有用。
Webhook 推送。数据更新完成后主动推送到指定地址,调用方不用轮询。适合做实时看板和自动化报表。
我个人在实际操作中的体会是,金融数据服务这件事,难点不在技术本身,而在细节的打磨。数据源的稳定性、字段的规范性、时区的处理、复权的准确性,每一个细节出问题都会导致上层应用出错。把这些问题一个个解决掉,积累下来就是一套可靠的基础设施。不要追求一步到位,先跑通最小闭环,再逐步完善,这样每一步都有反馈,不容易走偏。