场景引入:全市场信号扫描的「读」侧该怎么做
做 A 股技术信号分析,难点从来不在「算一次指标」,而在于「每天对全市场 5000+ 标的、50+ 技术指标的持续扫描与共振判定」。上游 stock-analyzer 已经把这件苦活干了——它定时跑 ETL,产出两个 SQLite:全市场信号/指标库 stock_analysis.db 与资产快照库 asset_snapshot.db。
真正缺的是「读」侧:一个能即时把这些结果变成可筛选、可排序、可对话的仪表盘的服务。很多实现会顺手把它写成一个 Node/Python 微服务,自己再维护一套连接池、缓存、鉴权——但 lume-scanner 选择了一条更克制的路:用单个 Lume 二进制在只读模式下把这两个库直接暴露成 HTTP API,前端用 React SPA 承接。下面是我落这个服务时最重要的几个决策。
决策一:单二进制架构与只读数据边界
lume-scanner 的业务入口集中在一个 scanner.lume 文件中——它用 Lume 的 server{} 声明了 11 个 /api/* 路由加一个 /discovery(信号共振、参数化筛选、个股 K 线、个股指标、180 天战绩、每日 Top 8 打分、模拟仓、实时行情、指数、元数据、数据新鲜度),通过 Lume C11 宿主提供 HTTP API,并把 React SPA 的静态资源一并部署。它跑起来就是一个进程、一个二进制(Lume C11 宿主,源码自建基座,随二进制自带量化内建家族——见下节),直接以只读方式打开上游那两个库查询。
这一点带来三个直接好处:
- 业务数据库只读。服务对上游两个库以只读方式打开,不回写任何上游数据;所有「信号/指标」都来自 ETL 的结果。这把「数据正确性」的责任边界切得很干净——扫出问题先看上游,不在这一层。
- 部署极简。没有连接池、没有 ORM、没有后台 worker。Docker 镜像就是单阶段把 Lume 二进制 +
scanner.lume+ 前端静态资源打进去,一条docker compose up就起。 - 安全默认。宿主进程默认绑定
127.0.0.1仅本机可达;在 Docker 内,容器进程会绑定0.0.0.0——但那是容器网络内部地址,并不意味着外网可达,真正决定暴露面的是宿主机端口映射与是否开启HTPASSWD鉴权。README 明确警告:绝不能把0.0.0.0与无 auth 同时暴露到宿主机——否则局域网里任何人都能匿名读全市场数据、还能烧你的 LLM key。
决策二:实时重算与量化指标交叉验证
上游 ETL 已经算好了一批指标,但 lume-scanner 在 /api/indicators/:code 这一层又用 Lume 内建实时重算了一遍 KDJ、Williams %R、ATR、OBV、MACD、RSI(两套口径)、Bollinger 等,并顺带给出绩效面板(Sharpe / Sortino / CVaR / VaR / 偏度 / 峰度)和 Lume 侧的信号判定。
为什么同一份数据要重算?关键在于 RSI 的双口径交叉验证:/api/signals 会同时给出 ETL 的 Cutler RSI 与 Lume 的 Cutler RSI,在已覆盖的测试数据和参数范围内,实测差值 Δ=0——这不是「差不多」,而是一个可断言的等式。需要把结论说准:它证明的是结果一致——Lume 的 Cutler RSI 与 ETL 逐位一致,为该实现的移植正确性提供了可重复验证的证据;它不等同于实现正确(还需独立参考实现、边界与异常输入的覆盖),更不意味着金融指标有效(计算正确不代表指标能预测或盈利)。再叠加 Wilder 口径做跨口径参考,工程证据包括 Python 参照值与多后端一致性测试。这套对照把「我移植的指标对不对」从信仰变成了能在响应里直接验证的事实。
技术底座:Lume 内建量化函数如何进入宿主
决策二里那些一行调用的 kdj(...)、macd(...)、sharpe_ratio(...) 不是这个项目现写的——它们是 Lume 自带的量化内建。家族的 GA 落在 lume-core(语言内核树)v0.11.0:波动率与风险(#57)、交易统计(#58)、规格与回归 harness(#59)、F 检验(#60)合进 builtins_math_*.inc 三大域片段(约 2800 行 C);随后 #54 补上 kdj / williams_r / atr / obv / rolling_max/min 这批技术指标序列,并 port 进宿主解释器;rsi_cutler 则是宿主侧为「逐位对账 ETL」专门写的内建(#56)。lume-scanner 的镜像基座从宿主源码构建,所以这三路——core 的 GA 家族、#54 的移植、宿主自写——全部落在同一个二进制里,DSL 直接可调:
- 技术指标与时间序列:
rsi(Wilder)/rsi_cutler(Cutler)/macd/bollinger/kdj/williams_r/atr/obv加滚动窗口家族; - 绩效与风险:
sharpe_ratio/sortino_ratio/value_at_risk/expected_shortfall/ 年化(收益/波动)/calmar_ratio/omega_ratio/kelly_fraction等; - 统计推断:
z_test/ttest_p/chisq_stat/f_stat(0.11.0 新增)与 p 值、bootstrap 置信区间、spearman/autocorrelation/ic; - 随机模拟:
sim_gbm/sim_ou/sim_merton/sim_portfolio路径模拟与 copula 分布; - 期权定价:Black-Scholes 解析价
bs_call/bs_put与希腊字母。
(以上五域完整函数清单见项目 README;lume-scanner 正文只用到其中一小角。)
lume-scanner 只用了其中一小角(摆动与趋势指标 + 绩效面板),但这正是「单二进制」决策的复利:所有算法都编译在同一个二进制里,/api/indicators/:code 从原始 K 线到 Sharpe 是一条进程内调用链——没有 Python 依赖、没有子进程、没有序列化边界。另一层保障是工程纪律,分两层看:core 侧,量化内建在三个后端(树遍历解释器、手写 LLVM IR 文本发射器、libLLVM C API 发射器)上有固定一致性测试集(tests/native_backends.sh),并由 tests/quant-refcheck.py 把结果钉死到 Python 参照值;宿主侧,rsi_cutler 这类宿主内建以「与 pandas ETL 逐位对账」为验收——Cutler 尾值按构造只依赖最近 w+1 根,与全历史列值 Δ=0(实测逐位一致)。所以「Lume 算的 Cutler RSI」和「Python ETL 算的 Cutler RSI」能做成 Δ≈0 的等式断言,不是巧合——是两边都被独立钉住了。
落到这篇文章想强调的三件事:实际使用的只有摆动/趋势指标与绩效面板一小角;可验证的计算能力来自跨后端一致性测试、Python 参照值与 ETL 逐位对账;扩展能力则由风险统计、随机模拟与期权定价兜底——这些内建不是为了堆数量,而是让 Lume 程序能在同一运行时内完成真实数值计算,并且可测、可验证。
决策三:大表查询的安全性与性能控制
只读不等于无脑读。上游库里有 180 天战绩,写稿时窗口函数扫一遍约 8.5M 行、耗时近 8 秒(行数随每日 ETL 增长),绝不能每次请求都重算。lume-scanner 在这层做了两件事:
查询侧:sql_query 这个 agent 工具在物理上只允许 SELECT——写操作根本进不来;所有带用户输入的筛选都走 ? 占位参数化(/api/screen 的白名单字段筛选就是这么做的),read_file 还锁死在 www/ 防目录遍历。
缓存侧:按更新频率分了多层——meta 5 分钟、signal-stats 30 分钟、daily-picks 10 分钟、RSI 60 秒、实时行情 15–30 秒、模拟仓 30 秒。这些 TTL 缓存是读密集场景的有意取舍:ETL 更新后,对应层最多可能 serve 一个 TTL 周期的旧数据,下游可借 /api/freshness 暴露的库最后更新时间自行判断新鲜度。前端(用 esbuild 打包成单个 app.js,约 220KB)还做了防御性 getJSON,遇到 500 返回的 HTML 能解析出错误原因而不是泛泛的 Unexpected token '<'。
最终落到一个 8 个 Tab 的仪表盘:信号扫描、规则筛选、个股、指标、模拟仓、AI 聊天、字段说明、数据源——其中 AI 聊天接了白名单只读工具(没配 LLM 时走离线 demo)。
lume-scanner 自己会算指标吗?
不会将重算结果持久化到数据库。指标分两类——上游 ETL 已经算好的直接读取;Lume 内建在 /api/indicators/:code 中实时重算(KDJ/Williams/ATR/OBV/MACD/RSI×2/Bollinger)用于交叉验证,不写回数据库。这些内建随 Lume 二进制自带(GA 于 lume-core v0.11.0,部分在宿主侧实现),不是项目手写的。
lume-core 的量化内建都覆盖什么?
五域——技术指标与时间序列(RSI 两口径/MACD/Bollinger/KDJ/Williams/ATR/OBV/滚动窗口)、绩效与风险(Sharpe/Sortino/VaR/ES/年化/Calmar/Omega/Kelly 等)、统计推断(z/t/卡方/F 检验、bootstrap 置信区间、IC)、随机模拟(GBM/OU/Merton 跳扩散/组合路径/copula)、期权定价(Black-Scholes 加希腊字母)。core 侧内建有跨三后端的一致性测试并钉 Python 参照值;宿主侧内建(如 rsi_cutler)以与 ETL 逐位对账为验收。
数据从哪来,多久更新一次?
来自上游 stock-analyzer 定时 ETL 产出的 stock_analysis.db 与 asset_snapshot.db。服务只读消费;/api/freshness 能查到两个库最后更新时间,资产快照停更类问题第一天就能暴露。
实时行情是自己的数据吗?
不是。服务端从公开行情文本源(免费、无需 key)拉,做 15–30 秒缓存后转给前端,不存储。
为什么强调只读和 127.0.0.1?
只读把数据正确性责任切给上游;127.0.0.1 仅本机可达,Docker 内容器才绑定 0.0.0.0(容器网络内部,不自动等于外网可达),真正暴露面取决于宿主端口映射与 HTPASSWD 鉴权。README 明确警告不可把 0.0.0.0 无 auth 暴露到宿主。
AI 聊天会写库或发外链吗?
不会。聊天只接白名单只读工具(查库 + 实时行情),未配 LLM 时走离线 demo。需要强调的是:模型生成的查询条件同样走服务端强制约束——sql_query 只允许 SELECT、用户输入走 ? 参数化,靠的是实现层校验而非提示词约束,所以自然语言查询也不至于越权写库或拖库。
这次实践验证了什么?
lume-scanner 不只是一个量化指标展示页面,也是对 Lume 应用开发路径的一次实际检验。
通过单一 Lume 宿主承载 HTTP API、只读 SQLite 查询、量化内建函数与 Agent 工具,应用减少了额外服务进程和跨进程调用边界。同时,通过跨后端测试、Python 参照值对照以及与上游 ETL 的逐位校验,让关键计算逻辑具备了可重复验证的依据。
需要把边界说清楚:lume-scanner 不生产信号——它只读消费上游 stock-analyzer 的 ETL 结果并验证其一致性;量化数据生产链(指标计算、信号评分)仍在 Python ETL 一侧,并未被取代。
这也不意味着单二进制架构在所有场景下都优于微服务,更不意味着指标计算正确就能产生有效的交易策略。它验证的是另一种工程选择:在合适的业务规模下,可以用更紧凑的运行时组合交付完整应用,并把计算正确性纳入开发与验收流程。
这也是 Lume 希望探索的方向:让 AI 编写的代码不仅能够运行,还能够通过明确的测试和验证机制建立可信度。
发表回复