Valence is a browser-based equity valuation workbench. It ingests filings, normalizes them into a canonical taxonomy, runs a driver-based five-year three-statement forecast, discounts unlevered FCFF at a CAPM-derived WACC, solves the reverse DCF, benchmarks public comps, audits the result against 23 accounting and model checks, and exports the whole thing to a 31-tab Excel workbook with live formulas.
Every published figure traces to an official filing, and anything that does not is labelled as something else rather than presented as a filing-derived number.
Every company has its own public URL. valence.sourabhpradhan.in/NVDA is a deep link that opens with the model already in the HTML, so the figures are readable before any script runs.
- Key Features
- The gates
- System Architecture
- Tech Stack
- Project Structure
- Getting Started
- Tests
- Core API Endpoints
- Licensing & Data
- Per-ticker deep links. Every covered company has a canonical page at
/stock/{slug}, with/NVDAas a short form that redirects to it. The model is server-rendered and revalidated hourly, so a shared link is a working link rather than an empty shell. - Driver-Based 5-Year Forecasting Engine: Income Statement, Balance Sheet, and Cash Flow across Base, Bull, and Bear scenarios, driven by operational metrics (Revenue Growth, EBITDA/EBIT Margins, CapEx % Revenue, D&A %, DSO, DPO, Tax Rate).
- Institutional DCF & WACC Buildup: full Free Cash Flow to Firm build with non-cash operating working capital, dynamic WACC (CAPM cost of equity plus tax-shielded cost of debt), Gordon Growth and Exit EV/EBITDA terminal values, and an Enterprise Value to Implied Share Price bridge.
- Dynamic Reverse DCF & 2D Sensitivity: solves for the market-implied perpetuity growth rate by exact closed-form inversion, paired with live two-way sensitivity grids over WACC against terminal growth and exit multiple. When no growth rate in a sane band can reproduce the market price, the solver returns nothing rather than a number it cannot justify.
- Public Trading Comps & Football Field Synthesis: benchmarks the company against sector peers (EV/Sales, EV/EBITDA, P/E, FCF Yield %, ROIC %) and synthesizes cross-methodology valuation ranges. Football field and comps are Excel-export features; the web workbench shows the DCF and the scenario matrix.
- Private Equity Exit Returns & IRR Waterfall: 3-year and 5-year Exit Equity Value, MoIC, and Equity IRR under each exit scenario with entry-price sensitivity grids.
- Multi-Market & Multi-Currency Support: US equities (NASDAQ/NYSE, USD millions) and Indian equities (NSE/BSE, INR crores), with currency and unit localization across all statements.
- 23-Check QA Engine: balance sheet balancing, cash flow reconciliation, debt schedule ties, share count consistency, DCF bridge tie-out, WACC validity, terminal growth below WACC, year-one growth plausibility, missing critical inputs, data provenance quality, historical reporting coverage, unit agreement within a model, and — the checks that decide whether a number may be published at all —
inputs_trace_to_a_filing,debt_is_actually_sourced, andvaluation_is_meaningful. A model with failing checks is still served, with the failures listed. A check that cannot run is marked skipped rather than forced to pass; where a skip carries a recorded diagnostic it is also held in the committed QA baseline, and the exported workbook shows the skip state and the numbers, never a blanket pass. - 31-Tab Interactive Excel Exporter: detail schedules drive the operating model through live Excel formulas (CAPM, FCFF sums, cross-sheet references, sensitivity grids, and live
=IF(...)audit checks). - Live Web Workbench: scenario switching, driver overrides with revert, methodology breakdown, and a model library kept in the browser.
These look like gaps and are not. They are recorded here so nobody "fixes" them.
- Annual filings only. SEC ingestion accepts facts from annual forms (
10-K,20-F,40-Fand their amendments) and keeps only duration facts spanning 330 to 400 days, so 10-Q quarters are never read. A DCF built on annual statements is the institutional standard, and mixing quarters into an annual-period forecast would misstate the growth anchor. Statements six to twelve months old is the correct reading, not staleness. - Throttled live ingestion.
VALENCE_INGEST_CONCURRENCYdefaults to 2. Measured on a single instance: one cached specification costs 1.15 MB resident, so a 50-entry cache is about 58 MB against 512 MB, and a live build costs 2.5 MB over roughly seven seconds. Memory is not the binding constraint. What remains is politeness toward the upstream filing and market-data providers, which rate-limit under concurrency. Raise it alongside a provider measurement, not a memory one. - Single-flight builds. A slug that is being built is not built twice, an unsourceable slug is negatively cached, and the cache is an LRU of 50.
- Prices self-heal. A live quote failure falls back to the last cached close preserving the original date, and the UI says so rather than presenting a stale number as current.
- A valuation is published only where a filing is behind it. Coverage is a sourcing problem, not an engine problem: a company ships if a filing contributed its historicals. After the NSE-majority sourcing fix in this commit, 14 of the 23 shipped models publish (9 US plus 5 major Indian listings — HCLTECH, INFY, L&T, ONGC, TCS); the rest are built but stay withheld, with the check that stopped them named on the page. A model that no filing is behind returns a verdict rather than a number.
- A DCF below market price is a view, not an error.
implied_price_deviation_is_explainableholds a deviation inside a band symmetric in both directions. Negative equity value for a filer with negative book equity is arithmetic, so it is reported as such rather than suppressed. - The filing beats the feed. Where a market feed and an official filing both carry a period, the filing's figures are what the model publishes. That precedence is explicit in the statement builder (reported rows outrank estimates) and pinned by tests, not left to load order. For Infosys the three sections foot to the filing's printed bottom line exactly in every year, and any caption the model does not consume is recorded with its reason rather than silently dropped.
Every defect found while building this engine was invisible from the inside. A model footed to the dollar on a debt figure 15% above the filing, balanced perfectly, passed every arithmetic check, and was wrong. A statement can tie across every identity and still be built on an input the issuer never published.
So the loop cannot ask "does it add up" — adding up is a property of the model. Correctness is a property of the inputs, and the only way to know an input is right is to compare it with something that does not share code with what it audits.
python scripts/audit_loop.py # all seven gates
python scripts/audit_loop.py --only tieout # one gate
python scripts/audit_loop.py --no-server # skip the live site gates| # | Gate | Oracle |
|---|---|---|
| 1 | tieout |
SEC XBRL, plus the filing's own rendered balance sheet for concepts us-gaap does not expose |
| 2 | excel |
The served API payload, read fresh, against the generated workbook |
| 3 | identities |
Arithmetic that must hold regardless of inputs |
| 4 | qa |
A committed baseline, so a NEW failure is distinguishable from a known one; skipped checks that carry a diagnostic are held in the same baseline |
| 5 | tests |
pytest and jest |
| 6 | self_check |
The Excel sheet contract and formula wiring, end to end |
| 7 | site |
A running server, over HTTP |
Ordered cheapest-and-most-decisive first: a tie-out failure invalidates every figure downstream, so there is no value in reading a workbook whose inputs are already wrong. Exit code is non-zero when any gate blocks.
Both taxonomies are read. A foreign private issuer filing a 20-F reports under
ifrs-full, and reading only us-gaap reported TSMC's cash as carrying no filed caption
at all — a gate reporting a disagreement that does not exist, which is as damaging as
one that misses a real disagreement.
On current main: tie-out 0 untied figures, 10 of 11 US filers audited clean
(the eleventh is TSMC, disclosed rather than counted), QA 0 regressions across 23
models, the backend and frontend test suites both green, and Infosys'
current-asset reconciliation landing exactly on the filed subtotal.
flowchart TB
subgraph Public["0. Public Web Tier (Next.js)"]
LP[Landing page /]
IDX[Ticker index /stock]
TKR["Per-ticker /stock/[slug]"]
RED["Short form /[ticker] 308"]
MET[Methodology /methodology]
end
subgraph Delivery["1. API Layer (FastAPI)"]
API[Router]
THR[Ingest throttle<br/>semaphore, single-flight, negative cache]
API --> THR
end
subgraph Ingestion["2. Ingestion & Storage"]
A1[SEC EDGAR XBRL<br/>annual forms only]
A2[Screener.in Excel]
A3[yfinance market feed]
Store[(SQLite universe + datapoints)]
A1 & A2 & A3 --> Store
end
subgraph Normalization["3. Taxonomy & Normalization"]
Mapping[Unified Taxonomy Mapper]
Canonical[Canonical Registry]
Derivation[Derived Metrics]
Slugs[Slug + CIK assignment]
Store --> Mapping --> Canonical --> Derivation
Store --> Slugs
end
subgraph CoreEngine["4. 3-Statement & Forecast Engine"]
Hist[Historical Assembly]
Fcst[5-Year Driver Roll-forward]
Schedules[Capex, D&A, NWC, Debt, Tax, Shares]
Derivation --> Hist --> Fcst --> Schedules
end
subgraph ValuationQA["5. Valuation & QA Core"]
WACC[CAPM WACC Module]
DCF[Unlevered FCFF Engine]
Reverse[Reverse DCF Solver]
Comps[Comps & Football Field]
Returns[PE Returns & IRR]
QA[23-Check QA Engine]
Schedules --> WACC & DCF
DCF --> Reverse & Comps & Returns
WACC & DCF & Reverse & Comps & Returns --> QA
end
subgraph Output["6. Output"]
XLSX[31-Tab Excel Exporter]
end
Public --> API
THR --> Ingestion
QA --> API
QA --> XLSX
classDef pub fill:#EFF6FF,stroke:#2563EB,color:#0f172a;
classDef ing fill:#F5F3FF,stroke:#7C3AED,color:#0f172a;
classDef core fill:#ECFDF5,stroke:#059669,color:#0f172a;
classDef val fill:#FFFBEB,stroke:#D97706,color:#0f172a;
classDef out fill:#FFF5F5,stroke:#DC2626,color:#0f172a;
class LP,IDX,TKR,RED,MET,API,THR pub;
class A1,A2,A3,Store,Mapping,Canonical,Derivation,Slugs ing;
class Hist,Fcst,Schedules core;
class WACC,DCF,Reverse,QA val;
class XLSX out;
- Core Engine: Python 3.12, Pydantic v2
- API: FastAPI, Uvicorn, Requests, HTTPX
- Data: SQLite, committed per-company model snapshots (
backend/data/cache/*.json) - Excel Renderer: OpenPyXL (live formulas via OpenXML value patching)
- Frontend: Next.js 16 (App Router), React 19, Tailwind CSS v4, TypeScript
Dependencies are declared in pyproject.toml, so pip install -e . is enough.
Valence/
├── backend/
│ ├── api/
│ │ ├── main.py # FastAPI app factory
│ │ ├── routes.py # Endpoints, slug allowlist, spec build & cache
│ │ ├── throttle.py # Ingest semaphore, single-flight, negative cache
│ │ └── static/ # Legacy SPA assets and icon
│ ├── data/
│ │ ├── ingestion/ # Source-specific parsers
│ │ │ ├── sec_edgar.py # US SEC XBRL company facts (annual forms only)
│ │ │ ├── screener.py # India Screener.in Excel parser
│ │ │ ├── india_live.py # Live India market data
│ │ │ └── us_live.py # Live US market data
│ │ ├── universe/
│ │ │ ├── models.py # Raw database schemas
│ │ │ ├── store.py # SQLite reader/writer, universe queries
│ │ │ ├── slugs.py # Public slug + CIK assignment
│ │ │ └── master_list.py # Covered tickers and markets
│ │ ├── pipeline.py # Ingestion orchestration
│ │ └── precompute.py # Model cache precomputation
│ ├── export/excel/ # 31-sheet openpyxl renderer
│ │ ├── builder.py # Low-level utilities & OpenXML string patcher
│ │ ├── exporter.py # Export runner
│ │ ├── render_front.py # Cover, guide, executive summary, tab manifest
│ │ ├── render_hist.py # Historical statements
│ │ ├── render_fcst.py # Operating model and schedules
│ │ ├── render_val.py # WACC, DCF, comps, football field, returns
│ │ ├── render_qa.py # Documentation and dynamic QA checks
│ │ ├── self_check.py # Sheet contract assertions
│ │ └── styles.py # Formatting and color tokens
│ ├── forecast/ # engine, assumptions, debt, share_count
│ ├── models/ # Pydantic contracts (spec, statements)
│ ├── normalization/ # taxonomy/ and financials/
│ ├── validation/ # accounting_checks.py, pipeline.py (CHECK_SUITE)
│ ├── valuation/ # dcf, wacc, reverse_dcf, comps,
│ │ # football_field, returns, sensitivity
│ └── tests/ # Pytest suite
├── frontend/
│ ├── src/app/
│ │ ├── page.tsx # Landing page
│ │ ├── layout.tsx # Metadata, tokens, browser surfaces
│ │ ├── stock/page.tsx # Ticker index
│ │ ├── stock/[ticker]/ # Per-ticker page, share card, JSON-LD
│ │ ├── [ticker]/ # Short-form redirector
│ │ ├── methodology/page.tsx
│ │ ├── not-found.tsx
│ │ ├── sitemap.ts, robots.txt/, manifest.ts
│ │ ├── icon.png, favicon.ico, apple-icon.png
│ ├── src/components/ # Workbench views, modals, landing blocks
│ ├── src/lib/ # API client, formatters, tickers, site config
│ └── public/media/ # Launch clip and poster
├── scripts/ # The launch gates (see The gates below)
│ ├── audit_loop.py # Runs all seven gates, prints a verdict
│ ├── tieout.py # Every bridge input vs the filing that published it
│ ├── qa_gate.py # Plausibility over every covered company, vs a baseline
│ ├── audit_valuation_figures.py # Served API payload vs the generated workbook
│ └── check_shipped_set.py # What the site serves vs what is committed
├── data/
│ └── qa_gate_baseline.json # Recorded failures, so a new one is distinguishable
├── .github/workflows/ # tests.yml, qa-gate.yml, and two market-data jobs
└── README.md
- Python 3.12+
- Node.js 20+ (the floor Next.js 16 requires)
There is no requirements.txt; the backend's third-party dependencies are:
pip install fastapi uvicorn pydantic openpyxl requests pdfplumber yfinancepytest is needed to run the suite.
git clone https://github.com/karbburn/Valence.git
cd Valence
python -m venv venv
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
pip install fastapi uvicorn pydantic openpyxl requests pdfplumber yfinance pytest
cd frontend && npm installOptional. Everything below has a working default.
# backend/.env
SEC_CONTACT_EMAIL=you@example.com # SEC requires a monitored contact in the User-Agent
TWELVEDATA_API_KEY= # optional market-data provider
VALENCE_INGEST_CONCURRENCY=2 # in-flight live builds
VALENCE_INGEST_NEGATIVE_TTL=900 # seconds to remember an unsourceable slug
# Off by default, and deliberately so. See "Indian filing acquisition" below before enabling.
VALENCE_ENABLE_NSE_ACQUISITION=# frontend/.env.local
NEXT_PUBLIC_API_URL=http://127.0.0.1:8111
# Optional. The canonical production host is pinned in the code; set this only
# to override it (for example when previewing against a staging origin).NEXT_PUBLIC_API_URL is read at build time, not only at runtime. The pages
are prerendered, so a build without it will find no companies to prerender and
the index will render empty.
Indian models are built from a market feed unless an audited statement has been
fetched for them. With VALENCE_ENABLE_NSE_ACQUISITION set, a company with no
cached statement is fetched from the exchange on demand, and the audited balance
sheet is read from it.
It is off by default, which is a decision rather than an omission:
- It runs on the read path, so enabling it means site traffic reaches an exchange.
- Request volume is governed by that exchange's terms, which have not been settled. Check them before turning this on.
- Without it the product degrades to a market feed, and says so on the page. That is a visible, labelled fallback, which is why it is acceptable.
When enabled, fetches are bounded three ways: a statement already on disk is never re-fetched, a failed attempt is remembered for five minutes per symbol, and a single process makes at most 25 attempts per hour. Note that one attempt may open several documents while looking for one that carries a balance sheet, so the real ceiling on requests is higher than 25.
Filings can also be fetched directly, which is how the committed sample documents were obtained:
python -m backend.data.ingestion.nse_filings TCS HCLTECH --refreshBackend:
python -m uvicorn backend.api.main:app --host 127.0.0.1 --port 8111Frontend, in a second terminal:
cd frontend
npm run dev| Route | What it is |
|---|---|
/ |
Landing page |
/stock |
Covered tickers, searchable |
/stock/NVDA |
A company's model, server-rendered |
/NVDA |
Short form, 308s to the canonical URL |
/methodology |
How the valuation is built, and what it cannot do |
/api/* is proxied from the frontend to NEXT_PUBLIC_API_URL, so the browser
never makes a cross-origin request.
curl -o model.xlsx "http://127.0.0.1:8111/api/export/excel?company_id=nvda_us"# The whole launch loop, in one command: seven gates, one verdict
python scripts/audit_loop.py
# Or individually
python -m pytest backend/tests -q # full backend test suite
python -m backend.export.excel.self_check # 31-sheet contract + formula wiring
python -m backend.api.self_check # Web API and recomputation
cd frontend && npm test && npx tsc --noEmit && npm run lint && npm run buildA commit that touches backend/ is not finished until the suite passes. The backend
suite takes about twelve minutes and, together with the tie-out gate, is the only thing
guarding the engine.
On every push to main, CI runs the backend suite, the frontend typecheck/lint/test/build,
and qa_gate.py in snapshot mode — offline, over the committed snapshots, against the
committed baseline. The gate fails on a new regression, not on the recorded failures,
so a permanently red gate is not a thing anyone learns to ignore.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Liveness |
GET |
/api/model/{company_id} |
Full ModelSpecification JSON |
POST |
/api/model/recompute |
Apply driver overrides, return updated model |
POST |
/api/model/revert |
Revert an override to baseline |
GET |
/api/export/excel |
31-tab .xlsx workbook |
GET |
/api/companies |
Covered companies |
GET |
/api/companies/manifest |
Paged universe with slug and has_model |
GET |
/api/companies/resolve |
Resolve one public slug to a company |
GET |
/api/companies/search |
Ticker and name autocomplete |
/api/companies/resolve is the security boundary for the public page routes.
/api/model/{company_id} will attempt live third-party ingestion for any
pattern-valid id, so a page route that passed unknown segments straight through
would let any well-formed URL start an ingestion. Unknown slugs 404 rather than
redirecting somewhere plausible.
Saved models live in the browser (localStorage). Nothing is uploaded or
synchronised.
Figures come from public filings (SEC EDGAR, NSE annual reports, Screener.in) and public market-data providers, and every figure on the site states which of those it came from -- a caption the filer itself prints is read from the filing, and one the filing does not carry is marked as coming from a market feed rather than presented as filed. Prices refresh daily. Valuation output is model-generated and is not a recommendation or investment advice.
Known and stated limits, rather than discovered by a reader:
- 10 of the 12 India models read a market feed rather than a filing, because the NSE annual reports have to be obtained and committed; only Infosys reads one today.
- where a caption is dropped rather than guessed, the reconciliation reports the shortfall instead of leaving it to be found.
- a period has one end date, taken from the filing; market feeds name columns by calendar month end and are not authoritative about when a fiscal period closed.
Built by Sourabh Pradhan.
