新闻详情

新闻详情

首页 / 资讯中心 / 详情

ETF量化交易系统实战:Web化升级与SQLite数据管理

发布时间:2026/10/1 18:00:39来源:尧图网络
ETF量化交易系统实战:Web化升级与SQLite数据管理
21天搭建ETF量化交易系统这个系列到DAY19已经算是进入收官阶段了。前面十多天我们把数据源、回测引擎、信号计算这些偏底层的模块陆续搭好大部分时间都对着终端和日志打交道。今天这一步要把整套系统从“命令行工具”升级成“Web应用”同时把本地数据管理模块彻底理清楚让系统真正进入“每天打开浏览器就能看状态、出信号”的状态。这篇内容我会按实际推进的顺序来写先解释为什么在DAY19把Web化提上优先级再详细拆本地数据管理模块的设计思路和表结构接着是轮动计算接口和页面的实现方式最后把联调阶段踩过的坑和排查过程整理成速查表。整个项目到这一步已经可以覆盖“数据入库、轮动排序、信号生成、结果展示”的完整闭环不再是零散的脚本集合了。1. 整体设计Web化为什么放在DAY19本地数据模块承担什么角色1.1 从终端输出到Web界面的动机在DAY19之前系统已经具备从数据源拉取ETF行情、计算动量排名、输出调仓信号这几个核心能力。但这些能力分散在几个Python脚本里每次运行都要在终端敲命令输出是纯文本表格看历史记录还得翻日志文件。对于每天只用几分钟查看系统状态的普通使用者来说这个交互方式不够直观。Web化要解决的不是“有没有功能”的问题而是“怎么看、怎么用”的问题。轮动系统的核心使用场景是每天早上打开页面看一眼当前持仓的ETF是否还在排名前列是否需要调仓最近几天的净值曲线是什么样的。这些信息如果变成浏览器里的表格、图表和状态标记使用门槛会下降一大截。我参考了一些现成的开源轮动策略项目在设计上做了两个决策后端采用本地服务模式不依赖外部数据库服务数据全部落在本机SQLite文件里启动系统就是启动一个本地Web服务端口固定自动化任务和手动查看共用一套数据。前端不引入重型框架使用服务端渲染模板加轻量图表库页面简化到“一屏看全”避免把精力耗在工程化前端上。这个取舍背后的逻辑很简单个人量化的Web系统核心价值在数据和逻辑展示不在交互复杂度。把数据层做扎实、把轮动规则算准确比做一个花哨的可视化界面重要得多。1.2 本地数据管理模块的定位和选型本地数据管理模块是整个Web版系统的地基。它要负责三件事ETF日线数据的存储与增量更新、轮动计算结果的快照保存、调仓记录的落库与查询。选型上我直接排除了MySQL和PostgreSQL原因是个人使用场景下完全没有必要引入独立数据库服务。SQLite单文件就能承载ETF日线数据、策略快照和交易日志不需要账号密码配置不需要额外进程备份就是复制文件。SQLite在这个场景下有几个实打实的优势数据文件直接放在项目目录下路径固定备份策略简单到拷贝一个文件就完成。Python标准库自带sqlite3模块不涉及ORM的额外配置和版本兼容问题。事务能力足够支撑“更新一批数据、写入一条调仓记录”这种轻量级写操作WAL模式的并发表现也够用。对数据库文件的管理我用一个独立的data_manager模块统一操作不散落在各个脚本里。模块内部只做数据的存取不做业务计算业务层通过接口调用数据层避免后续增加策略时改动数据库逻辑。2. 本地数据模块建表、增量更新、防重与校验的完整做法2.1 三张核心表的设计思路本地数据库我命名为rotation.db放在data目录下。整个数据模块围绕三张表展开实际建表语句如下-- ETF日线行情表 CREATE TABLE IF NOT EXISTS etf_daily ( ts_code TEXT NOT NULL, trade_date TEXT NOT NULL, close REAL NOT NULL, pct_chg REAL, volume REAL, amount REAL, PRIMARY KEY (ts_code, trade_date) ); -- 每日轮动快照表 CREATE TABLE IF NOT EXISTS rotation_snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, calc_date TEXT NOT NULL, rank INTEGER NOT NULL, ts_code TEXT NOT NULL, name TEXT, momentum REAL, is_hold INTEGER DEFAULT 0, action TEXT DEFAULT hold ); -- 调仓记录表 CREATE TABLE IF NOT EXISTS trade_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, signal_date TEXT NOT NULL, ts_code TEXT NOT NULL, direction TEXT NOT NULL, reason TEXT );etf_daily表的主键是ts_code加trade_date的组合这个设置非常关键它从数据库层面杜绝了同一只ETF在同一个交易日出现两条记录的问题。很多新手踩过的坑是用id做自增主键然后发现重复数据越积越多去重还得额外写语句。rotation_snapshot表保存每一天全市场ETF的动量排名全量结果字段里的is_hold标记当前是否持仓action记录操作动作buy、sell、hold。快照的意义在于事后复盘过了三个月想查某天的排名是怎么样的直接select这张表就能还原不用重新计算。trade_log表的结构偏向记录行为不在里面做过多冗余字段。调仓动作是buy还是sell信号日期是哪天触发原因是什么三个信息够用了。细节的成交价格和滑点估算留给后续的模拟交易模块去扩展。2.2 增量更新的实现逻辑数据源上面我用的是免费的数据接口方式从akshare拉取ETF历史行情。增量更新的思路是每次更新前先查etf_daily表里每只ETF的最新日期然后只拉取这个日期之后的数据避免全量重复下载。def get_latest_date(ts_code): cur conn.execute( SELECT MAX(trade_date) FROM etf_daily WHERE ts_code ?, (ts_code,) ) row cur.fetchone() return row[0] if row and row[0] else 20000101拿到最新日期后拼接开始日期参数调用数据源接口拉取增量数据。如果表里没有该ETF的记录就从默认的起始日期拉全量。这个逻辑保证了首次同步和日常增量走同一条代码路径不搞特殊分支。更新完成之后要做一次“校验”环节这是我在项目早期不多做、后来发现非常必要的步骤。简单来说就是检查拉取到的数据量和数据库里的记录数是否对得上检查最新日期是否等于数据源能提供的最新交易日。如果数据源返回为空大概率是接口返回格式变化或者网络问题这时候要主动报警而不是默默写一条空记录进库。2.3 数据异常与停牌处理ETF行情数据有个容易忽略的细节停牌期间数据源可能不返回记录也可能返回与前一交易日相同的收盘价。两种情况的处理方式不同。我采用的处理规则比较简单明确凡是数据源没有返回的交易日默认该ETF当日无成交不补录数据遇到持续多日数据缺失的情况在轮动计算时将其排除出候选池避免因价格长期不变导致动量指标失真。价格校验方面我会对单日涨跌幅绝对值超过15%的记录做人工复核标记。ETF的涨跌幅限制通常为10%超过这个范围基本可以判定是数据源错误需要排查复权因子或者字段映射问题。校验规则虽然粗糙但能把绝大多数脏数据挡在轮动计算之前。3. Web轮动系统FastAPI服务、动量计算接口与页面渲染3.1 服务端架构和项目目录Web服务我选用FastAPI纯粹是个人技术栈偏好。如果你更熟悉Flask替换成本也很低核心接口逻辑是一致的。我最终的项目目录结构如下etf_rotation/ ├── app.py # FastAPI入口 ├── data_manager.py # SQLite数据层 ├── momentum.py # 动量计算模块 ├── templates/ │ └── dashboard.html # 主页面模板 ├── static/ │ ├── echarts.min.js # 图表库 │ └── style.css └── data/ └── rotation.db # 本地数据库app.py里只做三件事提供查询接口、调用动量计算模块、把计算后的结果渲染到模板上。不写死任何业务规则所有参数从配置读取。3.2 动量轮动的计算逻辑动量轮动策略的核心思想是“强者恒强”过去一段时间涨幅靠前的资产未来一段时间继续跑赢的概率较大。实现上就是计算每只ETF过去N个交易日默认20日的区间涨跌幅按数值从大到小排序取前几名持仓。这里有一个参数细节需要说明计算区间涨跌幅用的不是“首日到末日简单相减”而是用累计每日收益率的方式公式如下def calc_momentum(close_prices, window20): if len(close_prices) window 1: return None segment close_prices[-max(window, 5):] # 在前复权数据基础上用区间内每日收益率累乘 ret 1.0 for i in range(1, len(segment)): if segment[i-1] 0: return None ret * (segment[i] / segment[i-1]) return ret - 1累乘法能更真实地反映区间内的复合收益。ETF的费率低、跟踪误差小这个方法对ETF的适用性比股票更好。实际运行时在数据源拉完数据后调用我实现的接口输出如下def run_rotation(calc_date): codes get_all_etf_codes() results [] for code in codes: closes fetch_close_series(code, calc_date, window20) if closes is None or len(closes) 21: continue mom calc_momentum(closes, window20) results.append((code, mom)) results.sort(keylambda x: x[1], reverseTrue) return results[:5] # 返回前5实际持仓取前3参数“前5”和“持有前3”是有意分开的。显示前5是为了让用户看到候补名单如果排第三的ETF近期风险指标恶化可以直接从候补里选顶上。这个设计比起只显示持仓名单可操作性好很多。3.3 调仓信号的生成规则调仓信号不是简单地“排名前3就买入排名掉出前3就卖出”真实场景中会出现排名频繁震荡的情况如果每天调仓交易成本会拖垮收益。我设置的规则参考了主流量化社区的做法加了一点自己的调整每5个交易日检查一次持仓排名周频调仓。持仓ETF的排名只要还在前5就不卖出只有跌出前5才触发卖信号。卖出后从剩余ETF中按排名补足持仓到3只。这个规则我回测过比“严格持有前3、掉出即卖出”的换手率低了约40%收益差别在可接受范围内。规则的具体代码写在momentum.py里与数据模块完全解耦后续想改成“连续N次掉出前5才卖”的版本只需要改判定函数。3.4 Dashboard页面的核心布局页面模板方面我保留了最简单的设计顶部是当前持仓状态卡中部是动量排名前10的表格底部是净值走势图。持仓状态卡展示的数据从rotation_snapshot表取最新日期记录字段包含持仓ETF名称、数量、最新收盘价、持仓浮盈比例。排名表格直接展示当天计算的排名全量数据每行标记“持仓”或“观察”状态让用户一眼看出当前信号。图表部分用了ECharts。考虑到动态数据的更新频率我直接在前端fetch一个接口返回的JSON数据来渲染折线图fetch(/api/nav) .then(r r.json()) .then(data { const chart echarts.init(document.getElementById(navChart)); chart.setOption({ xAxis: { data: data.dates }, series: [{ name: 策略净值, type: line, data: data.values }] }); });图表的净值数据由后端读取trade_log和etf_daily两张表按照“买入卖出价格、持仓数量变化”的模型计算逐日净值。这个过程不复杂但数据精度依赖成交价格的准确性所以我在trade_log表里特意加了reason字段记录每次调仓是基于什么排名变化触发的便于后续核对。4. 联调阶段与常见问题SQLite并发、数据重算、路径与编码问题4.1 SQLite在多线程写入场景下的表现FastAPI默认以多线程方式处理请求这意味着数据层可能被多个请求同时读取或写入。SQLite本身支持多读单写但在默认的rollback journal模式下并发写会出现database is locked错误。我的解决方案是在初始化数据库连接时启用WAL模式并设置忙等待超时conn sqlite3.connect(DB_PATH, timeout10) conn.execute(PRAGMA journal_modeWAL)WAL模式让读操作和写操作可以并发执行写操作之间仍然串行但通过timeout参数当多个写请求竞争时不会立刻报错而是等待锁释放。实测在单机个人场景下这个配置完全够用没有出现锁冲突影响业务的情况。更细一层的优化是在数据管理模块内部使用连接池每个线程池各自持有独立连接避免多个线程共享同一个sqlite3 connection对象。sqlite3默认check_same_threadTrue如果多线程共享连接会直接抛异常这个坑在Web服务里特别容易踩到。4.2 轮动结果的历史重算问题开发过程中经常要修改动量计算规则例如把窗口从20日改成10日或者调整调仓阈值。修改规则后之前保存的历史快照就“过期”了需要重新计算。这里我采用了简单有效的做法rotation_snapshot表里不保存计算时使用的参数版本而是在重算后删除指定日期范围的数据再重新插入。删除条件用calc_date字段控制不会影响行情数据表。def recalc_snapshot(start_date): cur conn.execute( DELETE FROM rotation_snapshot WHERE calc_date ?, (start_date,) ) for day in trading_days: snapshot compute_daily_snapshot(day) save_snapshot(snapshot)开发期重算是高频操作这种全部删除再重建的方式虽然简单粗暴但胜在逻辑清晰、不容易残留脏数据。等系统稳定运行后重算频率会大幅降低性能完全不是瓶颈。4.3 路径、编码与缓存三个容易忽略的细节先讲路径问题。项目里所有数据文件用相对路径会导致一个很隐蔽的错误通过命令行启动服务时当前目录在etf_rotation/下运行正常但如果用定时任务脚本调起当前目录或工作目录就变成用户目录了数据库路径就找不到了。我的解决方案是在模块顶部统一获取项目根目录的绝对路径BASE_DIR os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(BASE_DIR, data, rotation.db)这样无论从哪个工作目录启动数据库路径都是确定的不会出现“昨天还能启动今天怎么找不到数据文件”的诡异问题。编码问题通常出现在从接口返回的中文ETF名称写入数据库再读取展示的环节。连接SQLite时指定conn.execute(PRAGMA encoding UTF-8)更稳妥的方式是在读写接口层面做好统一的UTF-8编解码同时确保Web模板声明了正确的字符集。缓存问题则隐藏在前端。浏览器访问页面时JS文件和图表库可能被缓存导致更新了脚本但页面上看到的还是旧版本。开发阶段调试时常被这个问题困扰后来在静态文件请求后加上了版本参数例如style.css?v20250119就彻底避开了缓存干扰。这个技巧在正式部署到服务器做同局域网访问时也一样实用。4.4 常见问题排查速查表问题现象可能原因排查步骤与解法数据库报locked错误WAL模式未开启多线程写冲突执行PRAGMA journal_modeWAL连接设置timeout页面数据为空白快照表无当天记录计算未运行调用run_rotation接口生成当日快照ETF行情更新量与预期不符增量更新起始日期计算错误打印get_latest_date返回值比对数据源最新日期轮动排名异常收益过大未做前复权或除权导致价格跳变检查数据源的复权参数增量更新用前复权行情调仓触发过于频繁排名阈值设置过窄修改调仓规则为“跌出前5才卖”降低换手率前端页面中文乱码数据库编码或HTTP响应头不对统一UTF-8检查Content-Type和meta声明另外补充一个真实踩过的坑启动Web服务后首次访问行情数据加载很慢。原因是数据源接口对单只ETF拉取20日行情需要约0.3秒几十只ETF串行拉取就是十几秒。后来加了内存缓存把当日已计算过的动量结果存到内存字典里第二次请求直接命中缓存响应时间降到毫秒级。这个优化对日常使用体验提升很明显因为用户往往会在短时间内刷新页面多次。5. 一些开发过程中积累的实操体会做到DAY19这个节点我最大的感受是本地数据模块的质量直接决定整个Web系统的可靠性。如果数据层乱页面做得再好看展示出来的信号也没有参考价值。所以我在这两天花了大量篇幅来打磨数据校验、增量更新和快照保存这三个环节而不是急着堆功能。个人建议是在进入Web化阶段之前先回头检查一遍之前的脚本把“手动运行时能跑”和“被服务调用时稳定跑”这几个差异点找出来。比如路径是否绝对化、数据库连接是否管理好、异常日志是否完整这些问题在命令行模式下不太显眼一旦变成Web服务就会被放大成明显的故障点。DAY19完成后这套系统的日常操作已经可以控制在三分钟以内启动服务、打开页面、查看调仓信号。DAY20和DAY21我计划把自动更新数据、每日定时计算信号的任务调度加进来再补一个交易记录的手动确认功能把整个流程推到“自动化运行、人工确认执行”的状态。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于Simulink的光储微电网并网仿真:MPPT与SOC均衡控制 2026/10/1 18:46:51

基于Simulink的光储微电网并网仿真:MPPT与SOC均衡控制

做光储微电网并网仿真,尤其是想把光伏MPPT和蓄电池SOC均衡控制都塞进一个模型里跑通,我前后折腾了小一个月。这个项目看着不算复杂——无非是光伏板、Boost电路、蓄电池、逆变器、电网这些模块拼一拼,但真等你在MATLAB/Simulink里搭起来才发现…

阅读更多 →
C语言while循环详解:执行顺序、死循环避坑与工程实战 2026/10/1 18:46:45

C语言while循环详解:执行顺序、死循环避坑与工程实战

C语言里最不起眼、又最容易写崩的关键字,当属while。语法就那么一行,可无数人栽在它手里:该进循环没进、该停的时候停不下来、一跑就是满屏数字、风扇狂转。我第一次实际用while,是在实验室读一块串口传感器的数据,要求…

阅读更多 →
FPGA测试革命|新一代FPGA专用测试大模型NinthAI DV 2026/10/1 18:46:38

FPGA测试革命|新一代FPGA专用测试大模型NinthAI DV

NinthAI DV是深远华创(南京)信息科技有限公司联合战略合作伙伴共同推出的新一代FPGA专用测试大模型。NinthAI DV能够根据用户提交的需求和HDL代码,自动化的完成用户提出的测试要求,包括静态测试和动态测试,能够大幅度的…

阅读更多 →
微信小程序MD5中文加密不一致?先搞定UTF-8字符编码转换 2026/10/1 18:46:38

微信小程序MD5中文加密不一致?先搞定UTF-8字符编码转换

说实话,微信小程序里和MD5中文相关的这个bug,我在今年做的一个电商小程序项目里又踩了一遍。当时是接口签名校验通不过,后端用Java实现,前端在小程序里对同样的参数做MD5,两边算出来的值就是不一样。最让人抓狂的是&am…

阅读更多 →
货拉拉营销广告大模型落地实战:提示词工程与智能体工作流 2026/10/1 18:46:05

货拉拉营销广告大模型落地实战:提示词工程与智能体工作流

1. 货拉拉营销广告的真实痛点:为什么通用大模型直接拿来用会翻车 货拉拉的营销广告业务有个很鲜明的特点:它不是那种"一个品牌对全网喊话"的标准化投放,而是 同城货运场景下、司机端与货主端双角色、多城市多车型多时段 的碎片化…

阅读更多 →
TypeScript从入门到实践:类型系统、泛型与工程迁移指南 2026/10/1 18:46:05

TypeScript从入门到实践:类型系统、泛型与工程迁移指南

如果你写过一段时间的JavaScript,大概率经历过这种时刻:一个函数跑得好好的,换个调用方式突然就报错了;一段别人留下的老代码,改了一行数据格式,十几个地方跟着崩;又或者一个对象明明有某个字段…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉