Skip to content

Repository files navigation

Mercurius

English · 简体中文

A valuation tool for A-share and Hong Kong equities. Give it a ticker, get a value range — plus every step that produced it.

It does not give you a "target price." It gives you a range, the span each of five methods produced, the provenance of every parameter, and — when the data cannot support a conclusion — a refusal, with the reason.

600519.SH  Kweichow Moutai          engine 5.0.0
low 1,480.77   mid 1,922.70   high 2,885.29   price 1,289.50 · 2026-07-27

Composite            ├────────────────┤
DCF                  ├──────────┤
PE percentile           ├────────────┤
PB percentile             ├─────────┤
Dividend yield             ├───────────────────────────────┤
RIM       ├─┤                                     ← weight 0, dropped as outlier
                    ▲ price 1,289.50 (below the low end)

The stance

Every design decision in this tool traces back to one idea: epistemic honesty. It is enforced as seven hard rules (see docs/ARCHITECTURE.md §10):

Rule
§10.1 Historical multiples use unadjusted prices with point-in-time share counts, never adjusted prices
§10.2 No look-ahead: a reporting period becomes visible on its announcement date, not its period end
§10.3 Cross-currency requires explicit conversion with the rate and rate date recorded. No rate → refuse, never silently mix currencies
§10.4 Refusing to value beats valuing wrongly. If the data or sector premises don't hold, don't force a number
§10.5 The valuation layer is pure: no network, no database (enforced by an import-linter contract)
§10.6 Every actual computation is persisted as a snapshot (inputs + assumptions + outputs), reproducible and auditable
§10.7 All ingestion is upsert, therefore idempotent

Much of what looks like clutter in the UI is a direct consequence of these rules: the price always carries its observation date (it is not a live tick — it's the close of the most recent daily bar in the database); every assumption carries a provenance badge (derived from history / engine default / user override); a refusal spreads out the data that was collected alongside each method's own reason, instead of a bare "insufficient data."

Quick start

scripts/build.sh

Builds images → starts containers → waits for health → prints the URLs. Frontend at http://localhost:12100, backend at http://localhost:12101. See docs/DEPLOY.md.

First boot bootstraps the security roster (8,600+ A-share and HK tickers), treasury yields, and industry classification. Prices and financials are fetched on demand — they're pulled the first time you search a ticker, taking roughly 10–20 seconds.

Five methods

Method Basis When it refuses
DCF (FCFF) CFO + after-tax interest − capex; 5-year explicit window + Gordon terminal; three scenarios base-period FCFF ≤ 0, fewer than 3 annual reports, missing CFO/capex
PE percentile P25/P50/P75 of the stock's own daily PE history × current EPS negative EPS, under 3 years of history, coverage too sparse
PB percentile Same, against book value attributable to shareholders negative BPS (insolvent)
Dividend-yield percentile Inverts the historical yield percentile into a price (P75 maps to the low end) never paid a dividend, none in the last 12 months, too few paying years
RIM (residual income) Book value + discounted excess returns (Ohlson 1995), ROE fading to the industry median book value ≤ 0 (the balance-sheet anchor doesn't hold)

The composite range accepts only positive endpoints, weights them by confidence, and drops outlier legs. Fewer than 2 valid methods degrades the result and flags SINGLE_METHOD_RANGE; widely divergent methods flag METHOD_DIVERGENCE_ABOVE_2_5X.

Banks, non-bank financials, and real estate are refused outright — their liability structure distorts the FCFF basis, and v1 does not force a number.

Engine semantics are versioned (currently 5.0.0; SemVer plus a changelog in backend/app/valuation/version.py). When the version changes, old snapshots are invalidated automatically and recomputed with the new engine.

Architecture

Upstream (Eastmoney / Sina / Tencent / CNINFO / Yahoo, with source fallback chains)
  ↓ ingest/      idempotent upsert, serialized process-wide (DuckDB single writer)
DuckDB market.duckdb (prices, financials, share counts, dividends, FX, calendar)
  ↓ clean/       TTM differencing, announcement-date clipping, explicit FX, quality gates
FinancialSnapshot (frozen Pydantic)
  ↓ valuation/   pure functions: five methods + range composition
ValuationResult
  ↓ api/         snapshot persistence (SQLite app.sqlite) + uniform envelope
Frontend (Vite + React + TS)
  • Backend Python 3.12 / FastAPI / Pydantic v2 / uv
  • Frontend React + TypeScript + Tailwind v4
  • Specs OpenSpec — 20 capabilities, 100+ requirements, under openspec/specs/

Tests

uv run --project backend pytest -q     # 1189 passed
npm test --prefix frontend -- --run    # 277 passed
uv run --project backend lint-imports  # purity contract for the valuation layer

Backend tests are entirely offline — temporary DuckDB/SQLite, no network. Frontend fixtures are produced by the real backend models and the real engine, then written to disk, so assertions run against real response shapes. Workflow and conventions: docs/DEVELOPMENT.md.

Known limits

  • On-demand ingestion means only the stocks you've looked at have data. Whole-market screening requires a bulk pull first: ~5,800 A-shares takes hours (5 steps each, 0.5 s throttle, serialized process-wide).
  • Cheap cross-sectionally ≠ cheap historically. The PE/PB percentile methods compare a stock against its own history. A perennially low-PE toll-road operator ranks cheap market-wide while sitting at the top of its own range.
  • HK announcement dates are estimated (annual +3 months, interim +2 months), which is less precise than the real A-share dates. Flagged as raw.announce_date_estimated.
  • No HK trading calendar. akshare exposes only a mainland calendar, so HK quote-freshness falls back to an approximation. Fabricating one would be worse than having none — HK and mainland holidays only partly overlap.

Documentation

Start at docs/README.md — it routes by what you're trying to do, so you don't need to know a document's name first.

I want to…
deploy it locally docs/DEPLOY.md
call the HTTP API docs/API.md
change the code docs/DEVELOPMENT.md
fix something in production docs/OPERATIONS.md
understand why a design is the way it is docs/ARCHITECTURE.md

Contributing

Issues and PRs welcome. Read docs/DEVELOPMENT.md first — especially the seven hard rules. They are not style preferences; each one guards against a class of failure that silently produces wrong numbers. See CONTRIBUTING.md.

Note: code comments, documentation, and commit messages are written in Chinese; identifiers, type names, and log messages are in English.

Data sources

Prices and financials are fetched via akshare from public sources (Eastmoney, Sina, Tencent, CNINFO, and others). This project ships no data and resells no data — it fetches on demand and stores locally. Availability and terms of use are set by each upstream source and may change at any time; this project makes no guarantee about the accuracy or availability of that data. Confirm the licensing terms of each source yourself before any commercial use.

License

Apache License 2.0.

Disclaimer

This is a valuation calculator, not investment advice, and the author is not a licensed financial adviser. The ranges it produces depend entirely on the input data and the assumptions — both of which can be wrong. Upstream data may be missing or incorrect; the model assumptions may not fit a given company. Any investment decision is yours alone, as is the outcome.

Per Apache-2.0 §7 and §8, this software is provided "AS IS", without warranties or conditions of any kind, express or implied.

About

A-share & HK equity valuation: outputs a value range, not a target price — and refuses to value when the data can't support a conclusion

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages