资讯动态

从零构建开源股票监控系统:数据采集、指标计算与信号推送实战

发布时间:2026/9/24 13:10:55 来源:尧图企业网站定制
1. 为什么我要自己造一个OpenStock而不是继续用现成工具先说背景吧。几年前我盯盘的方式特别原始手机装两三个券商App电脑再开一两个网页版行情自选股跨了两个市场、四个板块每次想确认一只股票的均线位置、量能变化、资金流向至少要切三四个界面。最难受的是不同App的指标口径还不一样同一只股票这个显示金叉那个还在死叉区域根本不知道该信谁。所以当“OpenStock”这个概念出现在我面前的时候我几乎没犹豫就开始动手了。它不是一个官方出品的商业软件而是一套可以完全自己控制的开源思路数据自己收、指标自己算、界面自己画甚至连数据库都放在自己手里。说得直白一点这就是一个开源的、自托管的个人股票工作台。你不需要向任何第三方交付你的自选股、交易习惯和分析偏好所有的逻辑都跑在自己的服务器或者自己电脑上。1.1 市面工具的三大痛点在做OpenStock之前我仔细梳理过现成工具的痛点一共有三条每一条都挺要命。第一是口径不统一。同样是“20日均线”有的平台用复权价有的用原始价有的考虑了停牌日的处理方式不同最终画出来的线就是不一样。分红除权之后这个差异会被放大你做技术分析的时候信号点会前后漂移几百甚至上千个点完全没有参考价值。第二是数据不可导出。很多工具看行情没问题但你想把历史数据拉出来做一次回测或者把你的选股逻辑跑一遍数据被锁死根本无法导出。你只能手动去复制粘贴几百只股票一个个来效率低到离谱。第三是通知不可定制。你希望“某只股票放量突破20日高点”的时候提醒你或者“MACD在零轴上方金叉”的时候告诉你但大多数软件只能做简单的价格提醒稍微复杂一点的条件就无能为力了。这三点叠加起来就是你建一套私人股票监控系统最充分的理由。OpenStock的定位非常明确它不是一个交易软件也不是一个推荐股票的工具而是一个数据与分析的中台。它能做的是把行情数据完整地收下来统一标准地算好指标再按照你自己定义的规则输出信号通过微信、钉钉或者邮件推给你。1.2 OpenStock的目标边界动手之前一定要划边界否则很容易做成一个永远完不了工的大坑。我从一开始就给OpenStock定了三条边界。第一不做交易执行。也就是说OpenStock只负责告诉你“发生了什么”和“可能要发生什么”但绝不接券商交易接口帮你下单。原因是交易执行涉及的系统稳定性、安全审计、券商合规要求都太复杂不是个人项目该碰的。少了这块项目的复杂度直线下降我只需要把数据和分析做扎实。第二不追求毫秒级行情。个人自用的话行情延迟10秒甚至30秒都完全可以接受。我盯的是日线级别和小时级别的信号不是做高频量化。所以数据源不需要买付费的高速行情接口用免费的开源财经数据接口就够了。第三要能“断网自转”。这个系统的核心逻辑是“收了数据之后自己算”所以即便某个时刻数据源挂了或者网络波动了已经入库的数据还能继续支持指标计算和面板展示。这就决定了数据一定要落到本地数据库不能每次都实时拉取。2. 整条技术链路怎么设计的架构上的选择直接决定了这个项目后面好不好扩展。OpenStock的技术链路其实就四段数据接入层、存储层、计算层、展示层。每一层我踩过不少坑下面分别说。2.1 数据源选型聚合层是关键数据源是整个系统最底层、最关键的一环。我最早的想法是直接调某一家数据接口后来发现单一数据源有三个问题一是接口限流爬太频繁会被封二是字段不全有的给资金流、有的给融资融券但不同步三是数据质量不稳定偶尔会出现某一天某只股票的数据缺失。所以最终的方案是做一个聚合层也就是在业务代码和具体数据源中间加一个抽象接口。这个聚合层可以选择开源的数据接口库比如提供A股、港股、美股和基金数据的聚合库然后用“主备切换”的策略主力数据源拉取失败时自动用备用数据源补拉。对用户而言我只暴露一个统一的方法叫fetch_daily(symbol, date_range)底层是哪个数据源在提供服务上层完全无感。为了说清楚这个设计的好处我用一张表对比一下“直接写死数据源”和“聚合层方案”的区别对比项直接写死单一数据源聚合层方案接口失效时的恢复时间需要改代码、重新部署配置切换秒级生效字段统一性不同数据源字段名不一代码耦合严重统一数据模型转换在层内完成扩展性换数据源等于重写业务新增一个适配器即可限流处理被限流就停摆多源轮询分散请求压力聚合层听起来抽象实现起来其实就是每个数据源写一个Adapter适配器只要是能返回行情数据的公开接口都适配成统一结构。核心字段定死为ts_code证券代码、trade_date交易日期、open、high、low、close、pre_close、volume、amount。这样的好处是再往上层走所有代码都只认这一套字段跟具体数据源彻底解耦。2.2 存储选型别一上来就上大数据库存储这块我最初很纠结一上来就想用PostgreSQL觉得开源免费还稳定。后来实际跑了一周数据发现一个问题一个只有几十只自选股的个人项目单日行情数据量甚至不到几千行用PostgreSQL有点杀鸡用牛刀。而且如果只是自己用PostgreSQL的安装维护成本、内存占用、备份策略都成了负担。最终我选择了SQLite作为主存储理由是单文件存储备份直接拷贝文件就行零配置文件不需要单独装服务对OpenStock这个量级的数据完全够用几十万行数据做条件查询基本是毫秒级当然我也留下了升级路径。在数据库访问层做了封装如果未来数据量涨到一定规模可以很平滑地把存储引擎换成MySQL或PostgreSQL。对于个人项目来说这种渐进式的选型思路是合理的一上来就把架构拉到最重反而会消耗你宝贵的迭代精力。表设计上核心一张表就够了。我用的是“一表全维度”的策略把OHLCV和涨跌幅都放进同一张表CREATE TABLE daily_kline ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts_code VARCHAR(16) NOT NULL, trade_date VARCHAR(10) NOT NULL, open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL, pre_close REAL, volume REAL, amount REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(ts_code, trade_date) ); CREATE INDEX idx_kline_date ON daily_kline(trade_date); CREATE INDEX idx_kline_code ON daily_kline(ts_code);关键就在那个UNIQUE(ts_code, trade_date)约束上。它保证了同一只股票同一天不会插入重复数据为后面的增量更新打下基础。2.3 任务调度增量更新比全量抓取更重要行情数据的更新永远应该是增量的不是全量重刷。第一次历史数据回填的时候做全量之后每天收盘后做增量。增量更新的核心是弄清楚“上次更新到了哪天”这个状态我是记录在一张meta表里面的。调度框架我没有用Airflow这类重量级工具对个人项目来说Cron或者系统自带的定时任务就够用了。我是在一个Python模块里把所有更新任务写成一个命令行入口# 每日下午6点更新当日行情 30 18 * * * cd /opt/openstock python -m openstock.cli update_daily # 每周五晚上7点更新周线指标 0 19 * * 5 cd /opt/openstock python -m openstock.cli update_weekly这一层设计的中心思想是把要做的动作拆成一个个独立的小命令然后用操作系统的定时能力把它们串起来而不是在一个常驻进程里写一堆定时循环。好处是每个任务执行完就退出不存在内存泄漏也不会因为一个任务异常拖垮整个进程。3. 手把手搭数据层采集、清洗、入库这一节是最硬核的部分也是最容易被忽略的地方。我见过很多人搭类似系统一上来就关注画图好不好看结果数据质量一塌糊涂。但OpenStock这个项目数据层的质量决定了一切。3.1 最小环境依赖清单先列一下我的实际运行环境这是经过压缩之后的最小可用清单缺一不可组件版本用途Python3.10主开发语言财经数据接口库使用最新稳定版聚合行情数据源pandas1.5数据清洗与指标计算SQLitePython内置结构化存储FastAPI0.100供上层面板调用的数据接口前端图表库ECharts或同类行情走势可视化服务化的部分只有两个一个Python进程提供HTTP接口FastAPI一个定时任务模块做数据更新。没有引入Redis、没有引入消息队列、没有引入Docker的强制要求一台2核4G的小主机就能稳稳跑起来。3.2 行情采集代码的核心逻辑聚合层的核心代码大概是这样实现的我简化了异常处理的部分保留主链路给大家看import akshare as ak import pandas as pd from datetime import datetime, timedelta from typing import List, Dict class AkShareAdapter: 数据源适配器A股行情 def fetch_daily(self, ts_code: str, start_date: str, end_date: str) - pd.DataFrame: # 实际调用开源库不同数据源字段名不同这里做统一转换 raw_df ak.stock_zh_a_hist( symbolts_code.split(.)[0], perioddaily, start_datestart_date, end_dateend_date, adjustqfq # 前复权解决除权跳空问题 ) df raw_df.rename(columns{ 日期: trade_date, 开盘: open, 最高: high, 最低: low, 收盘: close, 成交量: volume, 成交额: amount, 涨跌幅: pct_chg, }) df[ts_code] ts_code return df[[ts_code, trade_date, open, high, low, close, volume, amount]]这里有一个非常重要的参数adjustqfq也就是前复权。如果不处理复权某只股票在除权除息日会出现一根假阴线或假阳线均线指标、涨跌幅统计全部失真。这也是很多商业软件和自建系统指标对不上的最大原因。3.3 清洗规则与增量更新实现数据拉下来之后清洗流程分四步每一步都有对应的判断规则去重基于(ts_code, trade_date)去重重复数据以后插入者为准。异常值过滤当日最高价低于最低价的直接标记为脏数据涨跌幅超过11%A股主板10%涨停创业板和科创板20%的情况要单独复核不属于绝对错误但需要记录到日志。缺失处理对非交易日周末、节假日系统拉不到数据属于正常不视为缺失。真正的缺失是某个交易日该有数据但没有这时候需要触发数据源切换或者补拉。类型统一成交量单位可能有的接口返回“手”有的返回“股”统一转换成“手”。增量更新的逻辑则依赖上面说的meta表。核心代码def get_last_update_date(ts_code: str) - str: # 从meta表读取上次更新日期没有则默认回退到3年前 pass def incremental_update(symbols: List[str]): for ts_code in symbols: last_date get_last_update_date(ts_code) # 如果今天是交易日且还没收盘就只更新到昨天 today_str datetime.now().strftime(%Y%m%d) if is_trading_day(today_str) and datetime.now().hour 15: end_date (datetime.now() - timedelta(days1)).strftime(%Y%m%d) else: end_date today_str start_date last_date df adapter.fetch_daily(ts_code, start_date, end_date) clean_df clean_raw_data(df) save_to_db(clean_df) # 依赖UNIQUE约束实现幂等插入 update_meta(ts_code, end_date)注意这个“幂等插入”思路即使同一个更新任务被重复触发由于数据库有唯一约束重复数据不会产生脏数据。这个设计让我在后面部署定时任务时非常有底气因为不需要关心任务重叠、手动补跑会不会出问题。4. 信号策略模块把行情变成买卖提示数据入库之后OpenStock的下一步就是算指标、出信号这是整个系统最有存在感的部分。一个只会展示K线的工具没有灵魂能按照自己的逻辑发出提醒才是自建系统的价值所在。4.1 技术指标计算的坑指标计算这块我一开始就决定不用自己手写公式而是用开源的技术指标库。原因很简单自己写指标公式容易在边界条件上出错比如上市第一天的数据算20日均线是补齐20天再算还是从第1天就开始算这个细节不同实现算法结果差别很大。这里要重点说一个坑使用第三方指标库时不同版本对NaN值的处理方式不一样。我曾在一次升级中指标库版本从0.19升到0.20结果RSI指标的结果整体偏移了原因就是新版对前N天的启动数据填充逻辑变了从“NaN填充”改成了“零值填充”。这个坑排查了很久最后是把版本固定住并且写了单元测试来锁定指标结果。使用指标库时指标计算逻辑大概是这样的import talib def compute_indicators(ts_code: str) - pd.DataFrame: df load_klines(ts_code) # 10日和20日均线 df[ma10] talib.SMA(df[close], timeperiod10) df[ma20] talib.SMA(df[close], timeperiod20) # MACD df[macd], df[macd_signal], df[macd_hist] talib.MACD( df[close], fastperiod12, slowperiod26, signalperiod9 ) # RSI df[rsi14] talib.RSI(df[close], timeperiod14) return df这个模块在设计上有一个原则指标计算必须是纯函数也就是同样的输入一定得到同样的输出。这样回测时什么行情出什么信号可以完全复现不会因为时间前后影响结果。为了让计算过程更快我对所有自选股做了批量并行计算核心方法就是按股票代码分组合并一次性从数据库读取所有历史数据。4.2 信号聚合与通知推送指标算完之后信号就需要被定义、聚合、推送。OpenStock我预设了三套信号规则每一套都通俗易懂均线金叉5日均线上穿20日均线且20日均线方向向上。MACD零轴上方金叉MACD快线在零轴上方上穿慢线这通常比零轴下方的金叉更可靠。RSI超卖反转RSI14小于30后重新上穿30说明超跌后开始修复。信号规则是写在配置文件里的我不希望改一条规则要去改代码。所以做了一个简单的事件计算引擎每条规则是一个包含指标条件和逻辑运算符的字典计算引擎遍历所有历史数据找到满足条件的日期生成信号事件。信号事件产生后通过通知渠道推送。OpenStock我实现了两个推送渠道一个是通用的Webhook推送可以接到主流的群机器人另一个是邮件推送适合做日报。def check_signal(ts_code: str, rule: dict, latest: pd.Series, history: pd.DataFrame) - bool: if rule[type] ma_cross: ma_short history.iloc[-1][ma5] ma_long history.iloc[-1][ma20] prev_short history.iloc[-2][ma5] prev_long history.iloc[-2][ma20] return prev_short prev_long and ma_short ma_long and ma_long history.iloc[-10][ma20] ...注意最后一个条件ma_long history.iloc[-10][ma20]这代表20日均线不再是走平或向下而是开始翘头。很多人的金叉策略失效就是漏掉了对均线方向的要求导致在下跌趋势中反复接飞刀。5. 前端展示一屏看完全部自选股数据有了信号有了接下来是展示层。我的原则是展示层要轻轻到可以随时推到重来因为前端不是这个项目的核心资产但又是每天都会看的东西。5.1 快速搭建只读面板我选择用FastAPI提供只读JSON接口前端用一个单页HTML加开源的图表库画K线走势。为什么不用Vue全家桶和前后端分离的大工程因为这个项目我维护精力有限一个单页文件改起来最方便打开浏览器就直接用不涉及构建工具链。后端接口只有两个app.get(/api/portfolio) def get_portfolio(): # 返回分组后的自选股列表及最新行情 pass app.get(/api/kline/{ts_code}) def get_kline(ts_code: str): # 返回某只股票的K线数据和指标序列 pass前端核心就是两张图第一张是组合概览用一个表格展示每只股票的最新价、涨跌幅、信号状态第二张是单只股票的K线图叠加MA5、MA20、MACD副图一眼看到当前处于什么形态。5.2 行情刷新与接口设计行情展示的时机也有讲究不需要实时轮询。A股每个交易日固定几个时间点才有新数据9:30开盘、10:30、11:30午休、14:00、15:00收盘。所以在这些关键时间点刷新一次足够覆盖99%的使用场景。我在前端做了一个很简单的逻辑每5分钟拉一次接口同时在后端用functools.lru_cache做10秒缓存避免高频率请求被打爆。这里我要特别强调一个细节接口返回的字段要做“精简层”。数据库表里字段多但前端展示真正用到的可能就是十几项。我会在接口层做一次DTO数据传输对象转换绝不直接把整个数据表对象丢给前端这样既减少带宽又避免暴露内部数据结构。6. 上线部署与真实踩坑记录OpenStock的开发完成不代表结束真正的考验在上线之后。我在实际运行中踩了不少坑挑几个最值得说的讲一下。6.1 用systemd挂后台任务最开始跑定时任务我用的是crontab用了两周发现一个问题任务执行时如果恰好系统重启cron不会补执行任务如果因为网络故障卡住很久cron可能开启第二个实例两个更新任务同时跑数据就乱了。后来我改用systemd的timer来管理定时任务。相比cronsystemd timer有几个明确的优势可以设置Persistenttrue保证系统上次关机期间错过的任务在下次启动后自动补执行可以依赖systemd的ExecStart做进程锁避免同一个任务并发跑日志统一交给journalctl管理排查问题方便不用去翻各种log文件。[Unit] DescriptionOpenStock Daily Update Afternetwork.target [Service] Typeoneshot WorkingDirectory/opt/openstock ExecStart/usr/bin/python3 -m openstock.cli update_daily [Timer] OnCalendarMon..Fri *-*-* 18:30:00 Persistenttrue Unitopenstock-daily.service [Install] WantedBytimers.target改完systemd之后我不用再担心错过补跑、也不用担心并发问题这种把运维细节内建在系统机制里的方案非常推荐。6.2 数据断档、时区错乱、指标漂移这三个坑第一个坑是数据断档。某个数据源在一个长假后第一个交易日历史数据更新接口连续失败了三小时。因为OpenStock有主备切换的聚合层备用数据源补上了数据整个过程没有影响面板展示但我在日志里看到备用数据源的缺口标志和主数据源不同导致部分字段为空。最后排查发现是备用源的适配器里没有处理某些停牌股票的数据格式。从那以后我在适配器里加了一个“字段缺失抛异常”的强制校验不允许静默吞掉异常。第二个坑是时区错乱。我部署的服务器时区是UTC而A股数据用的是北京时间。一开始我直接在系统层面设置成Asia/Shanghai觉得就完事了。但后来发现Python的datetime.now()在某些容器镜像里还是返回UTC。这个坑的教训是代码里绝不能依赖系统的默认时区必须在所有时间操作处显式指定from datetime import datetime from zoneinfo import ZoneInfo TZ_CN ZoneInfo(Asia/Shanghai) def now_cn() - datetime: return datetime.now(TZ_CN)第三个坑是前复权数据的指标漂移。前面说了用前复权能避免除权跳空但它有一个副作用前复权的历史价格会随着每次分红除权而变化历史数据里的“收盘价”不是真实的成交价只是复权价。这导致一个问题如果你用历史数据做回测某次除权之后之前算出来的均线位置全部漂移了。解决办法是数据在入库之后保留原始不复权价展示和指标计算时实时复权。换句话说数据库里存原始价前端做复权逻辑这样历史信号永远不会因为新一次分红而发生变化。6.3 还要不要继续扩展OpenStock搭到现在说实话已经能稳定满足我的日常需求了。每天开盘前我打开面板看一眼自选股的整体状态收盘后看一眼信号推送需要复盘的时候直接在面板上翻历史K线。很多朋友问我要不要继续加功能比如回测框架、组合仓位管理、行业板块联动分析。我的建议是个人项目最重要的不是功能多而是稳定。与其不停堆新功能不如把现有功能打磨得足够稳。我给OpenStock规划的下一步只有三件事一是把Webhook通知的模板做得更细致支持不同信号类型推送不同格式的卡片二是给数据层增加更完整的健康检查如果有异常能第一时间告警给我三是把定时任务的状态可视化做一个简单的“今天更新了哪些数据、有没有失败”的日志页面。根据我个人几次大改版的教训自托管开源股票系统最忌讳的就是“为了技术而技术”。最初可能只是想看个行情后来却花了大量时间在折腾架构上。OpenStock能做到现在这个程度核心价值不是代码有多复杂而是它让我把所有行情信息、信号规则和数据管理都掌握在自己手里。真正的成就感不是“我搭了个系统”而是“这个系统每天帮我省掉了两个小时低效盯盘的时间”。如果再让我重新搭一遍我依然会从数据层做起先把字段规范、增量更新、复权逻辑理清楚然后才是指标和展示。这个顺序一旦错了后面回来改的成本极高这是踩过坑之后最想说的一句话。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价