Scenario: What the "read" side of full-market signal scanning should look like
The hard part of A-share technical-signal analysis was never "compute an indicator once"—it is the daily, continuous scanning and resonance detection across 5000+ tickers and 50+ technical indicators. The upstream stock-analyzer already does that heavy lifting: it runs ETL on a schedule and produces two SQLite databases—stock_analysis.db (full-market signals/indicators) and asset_snapshot.db.
What was missing was the "read" side: a service that turns those results into something filterable, sortable, and conversational in real time. Many implementations would casually turn this into a Node/Python microservice and then maintain a connection pool, cache, and auth of their own—but lume-scanner chose a more restrained path: a single Lume binary exposes those two databases as HTTP APIs in read-only mode, with a React SPA on the front end. Here are the decisions that mattered most when I shipped it.
Decision 1: Single-binary architecture and the read-only data boundary
The business entry point of lume-scanner is concentrated in a single scanner.lume file—it uses Lume's server{} to declare 11 /api/* routes plus a /discovery (signal resonance, parameterized screening, per-ticker OHLCV, per-ticker indicators, 180-day track record, daily Top-8 picks, paper portfolio, real-time quotes, indexes, metadata, data freshness), serves HTTP APIs through the Lume C11 host, and bundles the React SPA's static assets for deployment. Running it is one process, one binary (the Lume C11 host, built from source, with the built-in quant family shipped in the binary—see below), opening the two upstream databases read-only and querying them directly.
This yields three immediate benefits:
- Upstream databases are read-only. The service opens the two upstream databases read-only and never writes back any upstream data; every "signal/indicator" comes from the ETL output. This draws a clean line of responsibility for "data correctness"—when something looks off, you inspect upstream, not this layer.
- Minimal deployment. No connection pool, no ORM, no background workers. The Docker image is a single stage that bundles the Lume binary +
scanner.lume+ front-end static assets; onedocker compose upbrings it up. - Secure by default. The host process binds
127.0.0.1by default and is reachable only on localhost; inside Docker the container process binds0.0.0.0—but that is a container-network-internal address and does not by itself mean the service is reachable from outside. What actually determines the exposure surface is the host port mapping and whetherHTPASSWDauth is enabled. The README explicitly warns against exposing0.0.0.0with no auth on the host—otherwise anyone on the LAN can read the full market anonymously and burn your LLM key.
Decision 2: Real-time recomputation and quantitative indicator cross-validation
The upstream ETL already computed a batch of indicators, but lume-scanner's /api/indicators/:code layer recomputes them live with Lume's built-ins—KDJ, Williams %R, ATR, OBV, MACD, RSI (two calibers), Bollinger, and more—and additionally returns a performance panel (Sharpe / Sortino / CVaR / VaR / skewness / kurtosis) plus Lume-side signal judgments.
Why recompute the same data? The crux is the dual-caliber RSI cross-validation: /api/signals returns both the ETL's Cutler RSI and Lume's Cutler RSI, and within the covered test data and parameter ranges the measured difference is exactly Δ=0—not "close enough," but an assertable equality. Be precise about the claim: what it proves is result consistency—Lume's Cutler RSI matches the ETL bit-for-bit, providing repeatable evidence for the porting correctness of this implementation; it is not the same as implementation correctness (which still needs an independent reference implementation and coverage of boundary and anomalous inputs), nor does it imply financial-indicator validity (correct computation does not mean the indicator predicts or profits). A Wilder caliber is layered on top for cross-caliber reference, and the engineering evidence includes Python reference values and multi-backend consistency tests. This turns "is my ported indicator correct?" from a matter of faith into a fact directly verifiable in the response.
Technical substrate: how Lume's built-in quant functions reach the host
Those one-line calls in Decision 2—kdj(...), macd(...), sharpe_ratio(...)—were not written for this project; they are Lume's built-in quant primitives. The family reached GA in lume-core (the language-kernel tree) v0.11.0: volatility & risk (#57), trading statistics (#58), the spec & regression harness (#59), and the F-test (#60) landed in the builtins_math_*.inc domain fragments (roughly 2,800 lines of C); #54 then added the technical-indicator sequences (kdj / williams_r / atr / obv / rolling_max/min) and they were ported into the host interpreter; rsi_cutler is a host-side builtin written specifically to reconcile bit-for-bit with the ETL (#56). lume-scanner's image base is built from host source, so all three streams—core's GA family, the #54 port, and host-side additions—live in the same binary, callable directly from the DSL:
- Technical indicators & time series:
rsi(Wilder) /rsi_cutler(Cutler) /macd/bollinger/kdj/williams_r/atr/obvplus a rolling-window family; - Performance & risk:
sharpe_ratio/sortino_ratio/value_at_risk/expected_shortfall/ annualized (return/volatility) /calmar_ratio/omega_ratio/kelly_fraction; - Statistical inference:
z_test/ttest_p/chisq_stat/f_stat(new in 0.11.0) with p-values, a bootstrap confidence-interval family,spearman/autocorrelation/ic; - Stochastic simulation:
sim_gbm/sim_ou/sim_merton/sim_portfoliopath simulation and copula distributions; - Option pricing: Black-Scholes analytic prices
bs_call/bs_putplus the Greeks.
(The full function list across these five domains is in the project README; this article uses only a small corner of it.)
lume-scanner uses only a small corner of this family (swing & trend indicators + the performance panel), but that is exactly the compounding return of the "single binary" decision: every algorithm is compiled into the same binary, so /api/indicators/:code goes from raw OHLCV to Sharpe as one in-process call chain—no Python dependency, no subprocess, no serialization boundary. The other safeguard is engineering discipline, in two layers: on the core side, the quant builtins have a pinned consistency test suite across three backends (a tree-walking interpreter, a hand-written LLVM IR text emitter, and a libLLVM C API emitter), plus tests/quant-refcheck.py nailing results to Python reference values; on the host side, host builtins like rsi_cutler are accepted by bit-for-bit reconciliation with the pandas ETL—the Cutler tail value depends only on the last w+1 bars by construction, Δ=0 against the full-history column (verified digit-for-digit). So "Cutler RSI computed by Lume" and "Cutler RSI computed by the Python ETL" forming a Δ≈0 assertable equality is no coincidence—both sides are independently pinned.
The three things this article wants to stress: what is actually used is only a small corner of swing/trend indicators and the performance panel; verifiable computation comes from cross-backend consistency tests, Python reference values, and bit-for-bit ETL reconciliation; extensibility is backed by risk statistics, stochastic simulation, and option pricing—these builtins exist not to pad a count, but so a Lume program can perform real numerical computation in one runtime, and be tested and verified.
Decision 3: Security and performance control for large-table queries
Read-only is not the same as reading blindly. The upstream database holds 180 days of track record; at writing time a window-function scan swept roughly 8.5M rows and took nearly 8 seconds (the count grows with each daily ETL)—far too expensive to recompute per request. lume-scanner does two things at this layer:
On the query side: the sql_query agent tool physically allows only SELECT—writes simply cannot get in. Every user-influenced filter goes through ? placeholders (/api/screen's whitelisted-field screening does exactly this), and read_file is locked inside www/ to prevent directory traversal.
On the cache side: layers are split by update frequency—meta 5 minutes, signal-stats 30 minutes, daily-picks 10 minutes, RSI 60 seconds, real-time quotes 15–30 seconds, paper portfolio 30 seconds. These TTL caches are a deliberate tradeoff for a read-heavy workload: after an ETL update, an affected layer may serve up to one TTL period of stale data, and the downstream can judge freshness itself via the last-updated time the /api/freshness endpoint exposes. The front end (bundled by esbuild into a single app.js, ~220KB) also has a defensive getJSON that, on a 500 returning HTML, parses out the error reason instead of a vague Unexpected token '<'.
The result is an 8-tab dashboard: signal scan, rule screening, per-ticker, indicators, paper portfolio, AI chat, field glossary, and data sources—the chat wired to whitelisted read-only tools (offline demo when no LLM is configured).
Does lume-scanner compute indicators itself?
It does not persist recomputation results to the database. Indicators come in two kinds—those the upstream ETL already computed are read directly; Lume’s built-ins recompute live at /api/indicators/:code for cross-validation, without writing back to the database. Those built-ins ship with the Lume binary (GA in lume-core v0.11.0, some implemented host-side), not hand-written in this project.
What does lume-core's quant family cover?
Five domains—technical indicators & time series (RSI in two calibers/MACD/Bollinger/KDJ/Williams/ATR/OBV/rolling windows), performance & risk (Sharpe/Sortino/VaR/ES/annualized/Calmar/Omega/Kelly), statistical inference (z/t/chi-square/F tests, bootstrap CIs, IC), stochastic simulation (GBM/OU/Merton jump diffusion/portfolio paths/copulas), and option pricing (Black-Scholes with Greeks). Core-side builtins have cross-backend consistency tests pinned to Python reference values; host-side builtins (like rsi_cutler) are accepted by bit-for-bit reconciliation with the ETL.
Where does the data come from, and how often is it updated?
From the upstream stock-analyzer‘s scheduled ETL output stock_analysis.db and asset_snapshot.db. The service consumes them read-only; /api/freshness reports the last-updated time of both databases, so a stalled asset snapshot surfaces on day one.
Is the real-time quote data your own?
No. The server fetches it from a public quote text feed (free, no key required), caches it for 15–30 seconds, then forwards to the front end—nothing is stored.
Why the emphasis on read-only and 127.0.0.1?
Read-only pushes data-correctness responsibility upstream; 127.0.0.1 is localhost-only, and inside Docker the container binds 0.0.0.0 (container-network-internal, not automatically externally reachable)—the real exposure surface depends on host port mapping and HTPASSWD auth. The README explicitly warns against exposing 0.0.0.0 with no auth on the host.
Will the AI chat write to the database or post external links?
No. Chat only connects to whitelisted read-only tools (DB queries + real-time quotes); it runs an offline demo when no LLM is configured. One caveat worth stating: the query conditions the model generates still go through server-side enforcement—sql_query allows only SELECT and user input is passed via ? placeholders, enforced at the implementation layer rather than by prompt constraints—so natural-language queries cannot escalate to writing or dumping the database.
What this practice verified
lume-scanner is not just a quantitative-indicator display page; it is also a real test of Lume's application-development path.
By carrying HTTP APIs, read-only SQLite queries, built-in quant functions, and agent tools all within a single Lume host, the application sheds extra service processes and cross-process call boundaries. At the same time, cross-backend tests, Python reference-value reconciliation, and bit-for-bit checks against the upstream ETL give the critical computation logic a repeatable basis for verification.
The boundary must be stated clearly: lume-scanner does not produce signals—it only consumes the upstream stock-analyzer ETL output read-only and verifies its consistency; the quantitative data-production chain (indicator computation, signal scoring) still lives on the Python ETL side and has not been replaced.
This also does not mean a single-binary architecture beats microservices in every scenario, nor that correct indicator computation yields a profitable trading strategy. What it verifies is a different engineering choice: at a suitable business scale, you can deliver a complete application with a more compact runtime combination, and bring computational correctness into the development and acceptance workflow.
This is also the direction Lume wants to explore: to let AI-written code not only run, but also build credibility through explicit testing and verification mechanisms.
Related Reading
- Project repository: github.com/erishen/lume-scanner
- stock-analyzer: A-share full-market technical-signal scanner (51-indicator ETL pipeline + market-wide signal scan scoring)
- Lume: single-binary agent DSL server (.lume scripts + C11 host serving static sites / JSON APIs / SSE chat / agent tools / SSR)
- Lume-core: Lume's language core (lexer / parser / type-checker + hand-written LLVM IR text emitter / libLLVM C API dual emitters)
Leave a reply