ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手把手搭建OpenStock:自建开源行情数据看板全攻略

手把手搭建OpenStock:自建开源行情数据看板全攻略 先说个大实话行情软件装得越多越觉得数据不掌握在自己手里。广告弹窗、自选股同步慢、指标动不动就收费折腾一圈下来你会发现最舒服的方式还是自己搭一套。我最近一直用的这套开源项目 OpenStock就是专门干这个的——它把行情采集、数据存储、API 服务和可视化看板串成了一条完整的链路花一个晚上就能从零跑起来。这篇文章就按“手把手教你搭建 OpenStock”的思路把完整过程、踩坑记录和部署方案一次讲清楚适合有一定 Python 基础、想自建行情看板的技术爱好者参考。OpenStock 本身的定位不复杂它不是一个像 OpenStack 那样的云计算平台而是一个面向个人投资者的开源数据展示系统核心能力就是把公开行情数据聚合成自己的数据库再通过网页看板展示出来。整个项目没有用重型框架所有模块加起来也就几十个文件个人完全能维护。下面我从设计思路开始讲把每个环节为什么这么做、怎么做、遇到问题怎么处理都摊开来说。1. 先盘清楚OpenStock 的定位与整体设计思路1.1 它到底解决了什么问题做这个项目之前我每天打开行情软件至少三五次但越想越别扭你看盘软件很多底层数据接口都是封闭的你用它的客户端数据就在它手里想导出来做二次分析基本没门。更麻烦的是我手里有几个自选股组合想同时看 A 股、港股、指数的实时价格不同软件之间数据口径还不一致这边涨幅 3.2%那边显示 3.17%看着就心烦。OpenStock 的目标就是把这些痛点一次解决掉。它把数据源统一收敛到后端所有展示都基于自己存的数据库自选股、K 线、涨跌幅、成交量这些指标想看什么自己定义。从长期来看这套系统的价值不是省几十块钱会员费而是真正把数据资产握在手里。行情数据通过公开接口拿回来清洗后落地存储之后无论做统计分析、写监控告警还是接钉钉、企业微信推送都是自己说了算。很多人问直接装个 akshare 或者 tushare 本地跑不就行了吗说实话只做数据分析确实够用但你要的是“能看、能存、能对外提供接口、能和前端图表联动”的一整套服务。OpenStock 多出来的价值恰恰是后端服务化和可视化这部分。1.2 技术选型背后的考量这个项目我选的是 Python 做后端前端用 Vue3 生态里最轻量的一种写法图表用 ECharts。选 Python 不纠结因为股票数据这块的现成库基本都在 Python 这边像 akshare、pandas 处理数据非常顺。FastAPI 则是最近几年个人项目里用起来最舒服的框架自带 Swagger 文档写接口的时候不用额外花时间维护文档数据校验用 Pydantic 也顺手。数据库没有上 MySQL 或者 PostgreSQL而是直接落在 SQLite。为什么个人使用场景下数据量一天撑死也就几万条增量SQLite 完全扛得住而且零运维、单文件备份整个系统复制到另一台机器上拷个 db 文件就完成了迁移。等哪天真觉得性能不够了再迁移到 PostgreSQL 也不迟ORM 层换一下就行。前端没有采用前后端分离的重工程结构而是用一个 FastAPI 静态文件目录把 HTML、JS、CSS 直接挂出去。这个选择在个人项目里非常务实省去了 Node 构建步骤改完前端代码刷新浏览器就能看到效果部署的时候也不用同时维护两个进程。图表用 ECharts因为它的折线图、K 线图交互很成熟缩放、十字光标、数据标签这些功能开箱即用社区例子也多遇到不会配的图表复制一个案例改改就能跑。1.3 整体架构与数据流整个系统从逻辑上分成四层我画了一个简化的调用关系采集层定时从公开行情源抓取实时报价和历史 K 线负责把原始数据变成结构化的 DataFrame存储层SQLite 数据库负责保存自选股列表、实时报价快照、日 K 历史数据API 层FastAPI 提供统一接口供前端页面和外部脚本调用展示层浏览器页面通过 ECharts 渲染行情走势和自选股表格数据流是这样的采集服务启动后先读取自选股配置文件然后请求公开行情接口拿到实时数据解析清洗后写入 SQLite历史 K 线则在每天收盘后通过增量更新的方式写入避免全量重复抓取。前端页面启动时调用 API 接口加载一次全量数据之后每隔若干秒只请求最新的实时报价通过图表和表格无刷新更新。这个设计的好处是层级解耦每一层都能单独改。比如你嫌腾讯行情源的字段不够多直接把采集层换成别的数据源API 层和展示层都不用动再比如你想加一个均线指标只需要在 API 层做一次聚合计算前端加一个 ECharts series 就行。2. 动手前的准备环境、依赖与项目骨架2.1 基础环境安装我默认你用的是 Linux 服务器或者本地 Linux/Mac 环境Windows 也能跑只是数据库路径和 systemd 部分需要相应调整。先确认几个基础工具是否就位python3 --version git --version建议 Python 版本不低于 3.10因为 FastAPI 和 Pydantic 的新特性在低版本上会有兼容问题。没有 Python 的话Ubuntu/Debian 系统直接用 apt 安装注意别用系统自带的旧版本 Python 裸跑最好用虚拟环境隔离。接下来创建项目目录和虚拟环境mkdir -p /opt/openstock cd /opt/openstock python3 -m venv venv source venv/bin/activate虚拟环境这一步千万别省。我有一次图省事把依赖直接装在系统 Python 里后来另一个项目要装不同版本的 requests直接互相污染排查了半个小时才找到原因。个人项目虽然规模小但环境隔离的习惯从一开始就养好后面省很多事。2.2 数据源评估与合规提醒做行情采集第一个要解决的问题就是数据从哪来。目前国内公开的免费行情源主要有两个新浪财经和腾讯财经的 HTTP 接口。腾讯的qt.gtimg.cn接口返回实时报价新浪的hq.sinajs.cn接口也是老牌方案。两个接口都支持批量请求一次请求可以带多只股票代码只是返回格式略有差异。实时报价我推荐腾讯接口字段用~分隔解析起来稳定性比较好数据更新速度也能到秒级。历史 K 线则直接用 akshare 封装好的接口它对不同市场的股票做了很多兼容处理省得自己给每个市场写解析逻辑。这里必须多说一句合规问题以上数据源都是公开的行情展示接口仅限个人学习研究和少量请求使用。不要拿去做商业分发也不要用高并发脚本反复抓取否则很容易被限流甚至封 IP。做个人看板没问题但要有节制合理设置请求频率这就是我在后面会反复强调的“克制请求”原则。依赖安装直接写进requirements.txtfastapi0.110.0 uvicorn[standard]0.29.0 requests2.31.0 pandas2.2.0 akshare1.12.0 apscheduler3.10.0装依赖就一句命令pip install -r requirements.txtakshare 安装的时候会带很多依赖如果在国内网络环境下有些包下载慢可以换国内 pip 镜像源。装完最好验证一下版本确认没有依赖冲突。2.3 项目目录规划与配置设计项目结构我按功能模块划分不追求过于复杂的包结构但也不能把代码全堆在一个文件里。下面是我实际使用的目录结构/opt/openstock/ ├── requirements.txt ├── config.py # 全局配置 ├── main.py # FastAPI 入口 ├── collector/ │ ├── __init__.py │ ├── realtime.py # 实时行情采集 │ └── kline.py # 历史 K 线采集与更新 ├── database/ │ ├── __init__.py │ └── db.py # SQLite 连接与建表 ├── static/ │ ├── index.html # 看板页面 │ └── app.js # 前端图表逻辑 └── stock.db # SQLite 数据库文件配置文件单独放的好处是后续改端口、改自选股、改刷新频率都集中在一个文件里不需要翻代码。我习惯把配置项的注释写详细一点毕竟时间一长你很难记得每一个参数是干嘛用的# config.py STOCKS [600000, 000001, 601318, 300750] QUERY_INTERVAL 5 # 前端轮询接口的间隔秒 REFRESH_INTERVAL 15 # 采集层更新实时报价的间隔秒 DB_PATH stock.db KLINE_START_DATE 20240101 # K 线起始日期配置里我把自选股写成了一个列表前端展示和后端采集都从这份配置读取。后面如果你想把自选股改成数据库里维护也只需要动采集层和管理页面整体改动范围非常小。3. 核心模块实操从抓数据到画图表3.1 实时行情采集模块实时行情采集是整个系统的心脏它决定看板上的价格和数据是否及时。我用腾讯接口做实时报价采集先看一下原始返回长什么样。请求下面的地址https://qt.gtimg.cn/qsh600000返回内容是 GBK 编码的字符串类似这样v_sh6000001~浦发银行~600000~7.88~7.90~7.85~123456~...不同字段用~分隔常用的字段有1 表示市场标识2 是股票名称3 是代码4 是当前价5 是昨收6 是今开后面的还有成交量、成交额、买一卖一等等。采集模块要做的第一件事就是注意编码腾讯接口默认是 GBK直接用 requests 的 text 属性会得到乱码必须显式设置resp.encoding gbk。实时采集代码我这样写# collector/realtime.py import time import requests import pandas as pd def fetch_quotes(codes): symbols [] for code in codes: if code.startswith(6): symbols.append(sh code) else: symbols.append(sz code) url https://qt.gtimg.cn/q ,.join(symbols) resp requests.get(url, timeout5) resp.encoding gbk lines resp.text.strip().split(;) rows [] for line in lines: if not in line: continue value line.split(, 1)[1].strip().strip() fields value.split(~) if len(fields) 10: continue rows.append({ code: fields[2], name: fields[1], price: float(fields[3]), pre_close: float(fields[4]), open: float(fields[5]), volume: int(fields[6]) if fields[6] else 0, amount: float(fields[37]) if fields[37] else 0, update_time: time.strftime(%Y-%m-%d %H:%M:%S) }) return pd.DataFrame(rows)有几个细节容易踩坑一是代码前缀的判断不能只判断是否以 6 开头北交所、科创板代码规则不同个人自选股如果包含这些板块需要额外扩充判断逻辑二是 fields 的长度要先做校验避免某只停牌股返回的字段不足导致数组越界三是批量请求时代码数量不要太多我一般控制在 30 只以内超过就分批请求避免返回内容过大被服务端拒绝。3.2 历史 K 线采集与增量更新看板只有实时价格不够K 线趋势是判断走势的重要依据。历史 K 线用 akshare 拉取一行代码就能拿到 DataFrame# collector/kline.py import akshare as ak def fetch_daily_kline(code, start_date, end_date): df ak.stock_zh_a_hist( symbolcode, perioddaily, start_datestart_date, end_dateend_date, adjustqfq ) return df这里有个关键选择adjust参数要不要做复权。如果不复权遇到除权除息的日子K 线图上会出现价格断崖均线指标也会失真如果用前复权历史价格会按最新价格调整适合看趋势和技术指标。我做个人看板用的是qfq前复权这样和大多数行情软件的习惯一致。数据写入 SQLite 时要注意增量更新。全量更新每天跑一次虽然简单但数据量会越来越大接口请求次数也浪费。我的做法是先查表里最新的一条日期下次更新只抓取这个日期之后的数据然后追加到表里。逻辑不复杂但能有效减少重复请求def update_kline(code): conn get_conn() last_date conn.execute( SELECT MAX(日期) FROM kline WHERE code?, (code,) ).fetchone()[0] start last_date if last_date else 20240101 today time.strftime(%Y%m%d) if start today: return df fetch_daily_kline(code, start, today) if df.empty: return df[code] code df.to_sql(kline, conn, if_existsappend, indexFalse) conn.close()akshare 返回的列名是中文直接写入数据库没问题但 API 返回时最好转成英文字段避免前端 JS 读数据时还要处理中文 key。3.3 FastAPI 接口设计后端接口是整个系统的连接器。前端要看什么接口就提供什么我设计了三个核心接口/api/quotes返回自选股实时行情/api/kline返回单只股票的 K 线数据/api/status返回系统基本状态。用 FastAPI 实现起来很简洁# main.py from fastapi import FastAPI, Query from fastapi.staticfiles import StaticFiles import sqlite3 import json app FastAPI(titleOpenStock API) app.mount(/static, StaticFiles(directorystatic), namestatic) def get_conn(): conn sqlite3.connect(stock.db) conn.row_factory sqlite3.Row return conn app.get(/api/quotes) def get_quotes(): conn get_conn() rows conn.execute(SELECT * FROM realtime).fetchall() conn.close() return {data: [dict(r) for r in rows]} app.get(/api/kline) def get_kline(code: str Query(..., description股票代码), limit: int 120): conn get_conn() rows conn.execute( SELECT * FROM kline WHERE code? ORDER BY 日期 DESC LIMIT ?, (code, limit) ).fetchall() conn.close() data [dict(r) for r in rows] data.reverse() return {code: code, data: data}注意get_conn里面设置了row_factory sqlite3.Row这样查询结果才能直接转 dict否则前端拿到的是元组字段名要靠猜。FastAPI 的 Query 参数自动帮你完成了代码校验接口文档在/docs页面直接能看这也是我选 FastAPI 的一个重要原因。3.4 前端看板页面与图表渲染前端页面我用最朴素的方式实现一个 HTML 文件负责布局一个 JS 文件负责数据请求和图表渲染。页面结构分三块顶部是总览信息和刷新时间中间是自选股行情表格下方是当前选中股票的 K 线图。ECharts 的 K 线图需要的数据格式比较特殊是[开, 收, 低, 高]这种顺序。很多人在这一步容易搞乱前端死活画不出正常的蜡烛图往往就是数据顺序不对。代码如下// static/app.js const chart echarts.init(document.getElementById(kline)); async function loadKline(code) { const resp await fetch(/api/kline?code${code}limit120); const json await resp.json(); const dates json.data.map(d d.日期); const values json.data.map(d [ d.开盘, d.收盘, d.最低, d.最高 ]); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: dates }, yAxis: { scale: true }, series: [{ type: candlestick, data: values }] }); }实时表格的刷新则用setInterval定时调用/api/quotes每次拿到数据后局部更新表头和价格列。刷新间隔建议至少 5 秒以上太频繁会给后端和行情源增加不必要的压力。技术上完全支持 1 秒刷新但个人看板没必要反而容易被限流。整个系统跑起来以后你在浏览器打开http://服务器IP:8000/static/index.html就能看到行情看板了。默认端口是 8000如果想直接访问根路径可以在 FastAPI 里加一个根路由把 index.html 返回。4. 部署运行与日常维护4.1 本地验证启动把所有模块写完先在本地跑一遍确保流程通顺。启动顺序有讲究先手动执行一次采集程序确认数据库里有了数据再启动 FastAPI 服务。否则会出现“接口能用但没数据”的空转状态前端拿到空数组图表一片空白。我用一个简单的启动脚本把两步合并#!/bin/bash source /opt/openstock/venv/bin/activate python -m collector.realtime python -m collector.kline uvicorn main:app --host 0.0.0.0 --port 8000先同步一次数据再启动服务这样第一次打开页面就有完整内容。4.2 用 systemd 守护进程稳定后台运行本地验证通过后部署到服务器或者长期运行的机器上我推荐用 systemd 管理进程而不是nohup或者screen。systemd 的好处很多开机自启、进程崩溃自动重启、日志统一管理这几个特性对常驻服务来说太重要了。新建一个服务文件/etc/systemd/system/openstock.service[Unit] DescriptionOpenStock API Service Afternetwork.target [Service] Userwww WorkingDirectory/opt/openstock ExecStart/opt/openstock/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restartalways RestartSec5 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target然后执行sudo systemctl daemon-reload sudo systemctl enable openstock sudo systemctl start openstock这里踩过一个坑服务文件里的 User 字段一开始我用 root 运行结果 Python 代码里所有文件权限都是 root 的后来想用普通用户改文件内容发现根本没有写权限。建议单独建一个www用户来跑服务目录权限适当收紧安全性好也方便管理。4.3 定时任务与数据更新策略OpenStock 的更新策略分两部分实时行情是常驻服务里用循环加间隔控制的历史 K 线则适合用定时任务在收盘后跑一次。A 股收盘时间是 15:00我设定每天 16:30 更新一次日 K这时候当天的数据基本已经稳定再早容易拿到被修正前的数据。定时任务我直接用 APScheduler集成在 FastAPI 的启动事件里省了一个 crontab 的管理成本# main.py from apscheduler.schedulers.background import BackgroundScheduler from collector.kline import update_all_kline scheduler BackgroundScheduler() scheduler.add_job(update_all_kline, cron, hour16, minute30) scheduler.start()这样整个系统就一个进程搞定所有事情前端静态页面、API 接口、定时 K 线更新都在 uvicorn 里跑运维成本降到最低。实时行情采集没必要用定时任务因为它是持续循环的我用一个后台任务配合asyncio.sleep来实现代码里可以写成独立线程。要注意的一点是服务器时区最好设置成Asia/Shanghai否则 APScheduler 的定时时间会按 UTC 计算16:30 会变成第二天凌晨执行K 线数据就差了一天。很多开发环境默认是 UTC这个问题防不胜防。5. 常见问题与排查技巧实录自己搭系统遇到的问题比你想象的多而且很多问题在官方文档里根本不会写。我把这段时间踩过的坑按频率排了个序整理成速查表方便你按图索骥。现象可能原因排查与解决前端图表空白K 线数据为空或字段名对不上先访问/api/kline?code600000看返回 JSON确认数据库有数据再查前端字段行情数据全是 0腾讯接口字段位置解析错误手动请求接口把返回字符串按~逐段拆分核对字段索引中文乱码接口编码不是 UTF-8在 requests 中显式设置resp.encoding gbk请求多了就被封请求频率太高触发限流降低刷新频率采集层加缓存批量抓取时用time.sleep(1)间隔SQLite 报 database is locked多线程同时写入打开数据库时设置check_same_threadFalse写入加锁或使用连接串timeout10定时任务不执行服务器时区不对检查系统时区timedatectl 设置为 Asia/Shanghaiuvicorn 进程时不时退出内存不足或代码抛异常查看journalctl -u openstock日志确认异常堆栈逐个说几个典型的。第一个是乱码问题。用腾讯接口时如果忘记把 requests 的编码设置为 gbk你抓回来的字符串全是非法字符字段数量都对不上解析时很可能直接报错。这个问题好排查因为报错信息很明显但新手容易在 requests 的text和content之间反复纠结。其实不用记住一条经验北方的行情接口很多是 GBK遇到乱码优先设置编码而不是换请求库。第二个是 SQLite 并发写问题。FastAPI 是一个多线程模型前端定时刷新会并发读接口后台定时任务在更新 K 线时又要写数据库读写并发的时候 SQLite 很容易报database is locked。我的处理方案是读操作走默认连接写操作改成串行用scheduler单线程执行。最省事的办法是给数据库连接加一个参数conn sqlite3.connect(stock.db, check_same_threadFalse)但这样只是治标。真正稳妥的做法是每次操作都新建连接、用完立即关闭不让连接跨线程复用。个人项目的数据量不大这种方式性能完全够用。第三个是限流问题。很多人一看到行情接口就控制不住想 1 秒刷一次结果跑不了十分钟就被服务端限制。我自己的策略是实时报价 15 秒刷新一次前端页面 5 秒查询一次 API后端查询数据库不会打到行情源所以压力其实很小。换句话说请求行情源的是采集层前端页面只是读自己的数据库中间隔了一层缓存这个设计天然避免了对数据源的频繁请求。第四个是部署环境时区。这个真的特别隐蔽APScheduler 一直没反应我还以为是服务没启动。后来date一看服务器时间是 UTC比北京时间慢了 8 个小时任务当然不会在预期时间触发。解决办法也别偷懒直接改系统时区sudo timedatectl set-timezone Asia/Shanghai改了之后重启服务问题立刻消失。我自己的操作习惯是每加一个功能就先把日志打出来看一遍。OpenStock 的日志我用的是 Python 自带的logging分两个文件一个记录采集日志一个记录 API 访问日志。遇到问题先翻日志能少走很多弯路。比如行情采集偶尔会拿到空的 DataFrame这时候如果日志里记录了原始返回内容排查起来会快很多。再分享一个进阶技巧。如果觉得 ECharts 的 K 线图不够直观可以叠加成交量柱状图用两个grid和两个xAxis实现一个在上面画蜡烛图一个在下面画成交量。这个改造很多人想加其实原理不复杂就是 ECharts 的多坐标系配置。把成交量数据用第二个 yAxis 绑定缩放下面的 grid就能得到一个非常像专业行情软件的双栏图。数据存储方面SQLite 单文件的好处我前面说了但也要注意定期备份。我写了一个简单的备份脚本每天凌晨用cp把stock.db复制一份带上日期后缀再定期清理 30 天前的备份。数据是辛苦抓回来的宁可备份用不上也不能哪天真丢了再后悔。整个 OpenStock 搭下来我最大的体会是真正的工作量不在代码而在数据链路和各种边角情况的处理上。接口返回格式变了、某天数据源超时、除权除息后 K 线价格跳变这些才是日常维护里真正会反复遇到的问题。但从零把它搭起来再看着看板在自己写的系统里跑起来那种掌控感是直接用现成软件完全体会不到的。后面我打算给 OpenStock 加上简单的均线指标计算和异动提醒再考虑接入更多的数据源做交叉验证让这套个人看板越来越顺手。
返回列表