
1. 金融数据服务从零搭建的完整思路1.1 这个项目到底在做什么第一次看到financial-services这个项目名很多人会以为又是一个爬股票数据的玩具脚本。我最初也是这么想的直到真正把代码拉下来跑通才发现它的定位比想象中要扎实得多——它本质上是一套面向个人开发者和小型团队的金融数据服务底座把行情获取、数据清洗、指标计算、接口暴露这几件事串成了一条完整的链路。说白了它解决的是这样一个痛点你想做一个跟金融数据相关的小工具比如盯盘提醒、持仓分析、策略回测甚至只是一个简单的资产看板结果发现光是把数据搞到手并且搞干净这一步就能耗掉你大半精力。数据源格式五花八门字段命名不统一缺失值和停牌日处理起来全是坑等你把这些都填平了真正想做的业务逻辑反而没时间写了。financial-services的价值就在于它把这层脏活累活封装好了你拿到的是一个相对规整的数据接口可以直接在上面搭自己的东西。它适合的人群也很明确有一定编程基础、想快速验证金融类想法的独立开发者需要内部数据服务但不想从零造轮子的小团队以及像我这样平时喜欢折腾各种数据、想有个统一入口来管理行情和基本面数据的技术爱好者。如果你完全不懂代码那这个项目对你来说门槛偏高但只要你会写点 Python、能看懂 JSON上手并不难。1.2 为什么选择服务化而不是脚本化这里有个关键的设计取舍值得聊一聊。很多同类项目走的是脚本路线——你写个get_stock.py跑一次拿一次数据简单直接。但financial-services选择了服务化的路线把数据获取和消费拆成了两端中间通过接口通信。这个选择背后是有讲究的。脚本化的问题在于一旦你的使用场景变复杂比如多个工具都要用同一份数据、需要定时刷新、需要缓存来减少重复请求脚本就会迅速变成一团乱麻。你会在每个脚本里重复写数据获取逻辑缓存各写各的字段处理各改各的最后维护成本高得吓人。而服务化之后数据获取逻辑只存在一份所有消费方都通过统一的接口拿数据改一处就能全局生效。当然服务化也不是没有代价。它引入了额外的复杂度你得考虑服务怎么启动、接口怎么设计、并发请求怎么处理、数据怎么缓存。对于只是偶尔跑一次的简单需求这套东西确实显得重。但我的经验是只要你预期这个项目会持续用下去、会不断加功能那前期多花的这点架构成本后面会加倍省回来。financial-services显然是站在长期使用这个前提下来设计的。1.3 整体架构的分层逻辑把项目拆开看它的架构大致分成四层每一层职责清晰这种分层是我比较欣赏的地方。最底层是数据源适配层。金融数据来源很杂有公开的行情接口、有本地导入的 CSV、有第三方数据商的推送。这一层的作用是把这些不同来源的数据统一成内部标准格式屏蔽掉上游的差异。你换一个数据源理论上只需要改这一层上面的逻辑不用动。往上是数据处理层负责清洗、对齐、计算。比如把不同频率的数据统一到日线、处理停牌日的空缺、计算移动平均和波动率这类衍生指标。这一层是整个项目里逻辑最密集的部分也是最容易出 bug 的地方后面我会专门讲。再往上是服务接口层把处理好的数据通过 HTTP 接口暴露出去。这一层决定了外部怎么用你的数据接口设计得好不好直接影响到消费方的开发体验。最上面是调度与缓存层负责定时刷新数据、管理缓存生命周期。金融数据有个特点盘中数据变化快、盘后数据相对稳定所以缓存策略不能一刀切得按数据的时间敏感度来区分对待。2. 核心模块拆解与关键实现细节2.1 数据源适配统一入口是第一原则数据源适配层最核心的设计原则就一条对外只暴露统一的接口对内允许各数据源各显神通。我见过太多项目在这一步偷懒直接在业务代码里if 数据源A: 这样取 elif 数据源B: 那样取结果业务逻辑里到处是数据源的影子换源的时候改到崩溃。正确的做法是定义一个抽象基类把所有数据源都要实现的方法固定下来比如获取日线行情、获取标的列表、获取基本面字段。每个具体的数据源继承这个基类各自实现细节。业务层只依赖基类不关心背后是哪个源。from abc import ABC, abstractmethod class DataSourceBase(ABC): abstractmethod def fetch_daily_bars(self, symbol: str, start: str, end: str) - list: 返回统一格式的日线数据列表 pass abstractmethod def list_symbols(self) - list: 返回该数据源支持的标的列表 pass这里有个实操细节字段映射表要单独维护。不同数据源对同一个概念的叫法千差万别有的叫close有的叫closing_price有的叫收盘价。与其在每个数据源实现里硬编码映射不如抽一张配置表出来改起来一目了然。注意字段映射一定要做类型转换和单位统一。我踩过的坑是某个源返回的成交量单位是手另一个源是股混在一起算指标结果差了 100 倍排查了大半天才发现是单位问题。2.2 数据清洗缺失值和异常值怎么处理数据处理层里清洗是最考验经验的部分。金融数据天然不干净停牌、涨跌停、除权除息、数据源偶发丢包都会让数据出现空洞或跳变。处理不好后面算出来的指标全是错的。缺失值处理要分情况。如果是停牌导致的缺失正确做法是保留日期但标记为停牌而不是直接删掉这一行。因为很多指标计算依赖连续的时间序列你删掉一天时间轴就错位了。如果是数据源偶发丢包那可以尝试重新拉取拉不到再用前值填充但一定要打上填充标记方便后续追溯。异常值检测我一般用两种方法结合。一种是基于价格波动率的单日涨跌幅超过某个阈值比如 A 股主板 10%、科创板 20%就标记出来人工确认另一种是基于成交量的成交量突然放大或缩小到历史均值的极端倍数也值得警惕。这里的关键是标记而不是直接删除因为有些异常值其实是真实的市场行为比如重大消息刺激下的放量涨停删了就丢失了重要信息。def clean_daily_bars(bars: list, price_limit: float 0.1) - list: cleaned [] for i, bar in enumerate(bars): # 标记停牌 if bar.get(volume, 0) 0: bar[status] suspended cleaned.append(bar) continue # 检测异常涨跌幅 if i 0: prev_close cleaned[-1][close] change (bar[close] - prev_close) / prev_close if abs(change) price_limit * 1.5: bar[flag] abnormal_move cleaned.append(bar) return cleaned2.3 指标计算别小看移动平均的坑指标计算看起来简单实际上暗坑不少。就拿最基础的移动平均来说很多人直接一个rolling(window).mean()就完事了但金融数据里这么做会出问题。第一个坑是停牌日的处理。如果某只标的停牌了几天你的时间序列里这几天是空缺的直接 rolling 会把停牌前后的数据接在一起算得到的均线是失真的。正确做法是先把停牌日填充为前收盘价相当于价格没变再算均线这样均线才是连续的。第二个坑是复权。股票除权除息后价格会出现跳空如果不做复权处理均线、涨跌幅这些指标全都会失真。前复权、后复权各有适用场景做历史回测一般用前复权看当前持仓成本一般用后复权。这个选择必须在计算指标之前就定好中途换复权方式会让所有历史指标失效。第三个坑是窗口期的边界。序列开头的前 N-1 个点是没有完整窗口的这时候 rolling 会返回 NaN。有人图省事直接fillna(0)这是大忌会让指标在开头出现严重偏差。正确做法是保留 NaN或者用min_periods参数控制让窗口不满时返回 NaN消费方自己决定怎么处理。指标类型常见坑推荐处理方式移动平均停牌日导致窗口错位停牌日填充前收盘价后再计算涨跌幅未复权导致跳空统一使用前复权价格波动率窗口边界 NaN 被填 0保留 NaN用 min_periods 控制换手率流通股本变动未同步按日期匹配对应的流通股本2.4 接口设计让消费方用得舒服服务接口层的设计我的核心原则是让消费方少动脑子。接口返回的数据应该是开箱即用的不需要消费方再做二次清洗和格式转换。具体来说接口的 URL 设计要符合直觉比如/api/bars/{symbol}?start...end...这种一看就知道是拿某只标的的行情。返回格式统一用 JSON字段命名保持一致不要这个接口叫close、那个接口叫close_price。分页和限流也要考虑进去金融数据动辄几千条一次性全返回既慢又容易把消费方内存撑爆。# 获取某标的日线行情 curl http://localhost:8000/api/bars/000001?start2024-01-01end2024-06-30adjustqfq # 返回结构 { symbol: 000001, adjust: qfq, count: 120, bars: [ {date: 2024-01-02, open: 10.5, high: 10.8, low: 10.3, close: 10.6, volume: 1234567} ] }提示接口一定要带版本号比如/api/v1/bars/...。等你以后要改返回结构时老版本还能继续服务不至于把已经在用的消费方全部搞挂。3. 从零跑通的完整实操流程3.1 环境准备与依赖安装先把环境搭起来。我推荐用 Python 3.10 以上版本因为项目里用到了一些较新的类型标注语法。虚拟环境是必须的别嫌麻烦金融数据处理涉及的库版本冲突很常见隔离环境能省掉大量为什么我这里跑不起来的问题。# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install fastapi uvicorn pandas numpy requests pydantic这里解释一下几个关键依赖的作用。fastapi负责服务接口层性能好、自带文档uvicorn是跑 FastAPI 的服务器pandas和numpy是数据处理的绝对主力清洗和指标计算全靠它们requests用来拉取外部数据源pydantic负责数据校验保证进出接口的数据格式正确。注意pandas 的版本要留意。2.0 之后有些 API 变了比如append被移除、inplace行为有调整。如果你参考的教程比较老可能会遇到 API 不兼容的报错这时候要么降版本要么按新 API 改写。3.2 配置文件与数据源接入项目一般会有一个配置文件来管理数据源、缓存路径、服务端口这些参数。我习惯用 YAML 或者.env文件把配置和代码分离改配置不用动代码。# config.yaml server: host: 0.0.0.0 port: 8000 datasource: primary: local_csv csv_path: ./data/daily cache_ttl: 3600 cache: backend: sqlite path: ./cache/financial.db数据源接入这一步如果你手头没有现成的行情接口最省事的办法是先用本地 CSV 跑通流程。准备几份格式规整的 CSV字段包含日期、开高低收、成交量放到配置指定的目录下。这样你能先把整条链路跑通验证清洗、计算、接口都正常再去接真实数据源。这个先用假数据跑通再换真数据的思路是我做数据类项目一贯的做法能极大降低调试难度。3.3 启动服务与验证接口配置和数据都准备好之后启动服务就一行命令的事。uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload--reload参数在开发阶段很有用改了代码自动重启省得手动来回切。但上线时一定要去掉否则性能会受影响。服务起来之后先别急着写业务用浏览器打开http://localhost:8000/docsFastAPI 会自动生成交互式接口文档。在这里你可以直接点按钮测试每个接口看看返回的数据对不对。这一步非常关键我见过太多人跳过验证直接写业务结果业务出问题时不知道是数据的问题还是逻辑的问题排查起来两头堵。验证的时候重点看几个东西返回的字段名对不对、数据类型对不对价格是浮点、成交量是整数、时间范围是不是你请求的范围、复权方式有没有生效。这几项都对了说明数据链路是通的。3.4 定时刷新与缓存策略金融数据不是拉一次就完事了得定时刷新。项目里一般用调度器来实现比如APScheduler或者简单的while循环加sleep。刷新的频率要按数据的时间敏感度来定盘中数据可能几分钟刷一次盘后数据一天刷一次就够了。缓存策略是这里的关键。我的经验是分层缓存原始数据缓存时间长一些因为历史数据基本不变计算后的指标缓存时间短一些因为可能随参数调整而变化。缓存后端用 SQLite 就够个人项目用了轻量、免安装、支持 SQL 查询。import sqlite3 import time def get_cached_or_fetch(key: str, fetch_fn, ttl: int 3600): conn sqlite3.connect(./cache/financial.db) cur conn.cursor() cur.execute(SELECT value, ts FROM cache WHERE key ?, (key,)) row cur.fetchone() if row and time.time() - row[1] ttl: conn.close() return row[0] # 缓存过期或不存在重新获取 value fetch_fn() cur.execute( INSERT OR REPLACE INTO cache (key, value, ts) VALUES (?, ?, ?), (key, value, time.time()) ) conn.commit() conn.close() return value提示缓存 key 的设计要包含所有影响结果的参数比如标的、时间范围、复权方式、指标参数。少包含一个参数就可能出现改了参数但拿到旧缓存的诡异问题。4. 常见问题排查与避坑经验4.1 数据对不上怎么办这是最高频的问题你算出来的指标跟别处看到的不一样。遇到这种情况别急着怀疑代码按下面的顺序排查。先确认复权方式是否一致。前复权和后复权算出来的均线能差出一大截这是最常见的对不上原因。再确认时间范围是否一致有的数据源把当天算进去有的不算。然后确认停牌日处理是否一致有的把停牌日当交易日填充有的直接跳过。最后才去查代码逻辑。我整理了一张排查速查表遇到数据对不上时按这个顺序过一遍基本能定位到问题。现象可能原因排查方法均线整体偏移复权方式不同对比复权因子某几天数据缺失停牌日处理不同检查停牌日是否填充涨跌幅对不上基准日不同确认前收盘价取值成交量差整数倍单位不同手/股检查单位换算指标开头异常窗口边界 NaN 处理检查 min_periods 设置4.2 服务跑一段时间就卡死这个问题我遇到过好几次最后定位下来基本都是缓存没清理或者连接没关闭导致的。SQLite 连接如果忘了close()连接数会越积越多最后把服务拖死。解决办法是用上下文管理器确保连接一定被释放。from contextlib import contextmanager contextmanager def get_db(path: str): conn sqlite3.connect(path) try: yield conn finally: conn.close()另一个常见原因是内存泄漏。pandas 处理大数据时如果反复创建大 DataFrame 而不释放内存会持续上涨。解决办法是处理完及时del掉不用的变量或者把处理逻辑拆成小批次别一次性把几年的数据全加载进内存。4.3 接口响应慢的优化思路接口慢通常有三个原因数据量大、计算重、没缓存。优化也按这个顺序来。数据量大就分页别一次性返回几千条。计算重就预计算把常用的指标提前算好存起来接口直接读结果而不是每次请求都现算。没缓存就加缓存尤其是那些参数固定、结果稳定的请求缓存命中后响应能从几百毫秒降到几毫秒。我实测下来一个没做任何优化的接口返回一年的日线数据大概要 300 到 500 毫秒加上缓存之后重复请求能降到 10 毫秒以内。这个提升对交互式应用来说是质变。4.4 几个我踩过的坑第一个坑是时区问题。金融数据的时间戳如果不带时区跨时区处理时会出现日期错位。我的做法是统一用 UTC 存储展示时再转成本地时区。第二个坑是浮点数精度。价格用 float 存储累加计算后会出现0.1 0.2 0.30000000000000004这种问题。涉及金额精确计算时用Decimal或者整数分来存。第三个坑是并发写缓存。多个请求同时发现缓存过期同时去拉数据、同时写缓存会造成重复请求和写冲突。解决办法是加锁或者用只有一个请求负责刷新其他请求先用旧缓存的策略。第四个坑是数据源限流。免费数据源基本都有请求频率限制拉太快会被封。一定要在适配层加节流控制请求间隔并且做好失败重试和退避。5. 后续可以怎么扩展把基础链路跑通之后这个项目其实还有很大的扩展空间。我目前在自己版本上加的东西包括指标库的扩充除了均线还加了 MACD、RSI、布林带这些常用指标都封装成可配置的函数多标的批量处理一次请求能拿多只标的的对比数据简单的回测框架基于历史数据验证策略想法。再往远了想还可以接实时推送用 WebSocket 把盘中变化推给前端可以做数据质量监控定时检查数据完整性发现异常自动告警甚至可以加一层权限控制让不同的人只能访问自己权限内的数据。不过我的建议是别一上来就贪多。先把数据获取、清洗、接口这条主链路做扎实确保数据准确、服务稳定再考虑往上叠功能。金融数据这东西准确性永远是第一位的功能再多数据错了都是白搭。我在实际使用中最大的体会就是慢一点没关系错一点都不行。每次加新功能之前先花时间验证数据对不对这个习惯帮我省掉了无数次返工。