Does climate tech survive on its own, or does it depend on government subsidies and cheap financing? This engine tests that question through three scored mechanisms: SEC-filed balance-sheet leverage, the underlying engineering economics (LCOE) of the technology itself, and a regime comparison of price performance around the IRA's passage. Plus one descriptive mechanism (raw market-regime context around interest-rate cycles) that couldn't clear a genuine significance test and is reported honestly as color, not dressed up as a verdict. The disagreements between lenses are usually the interesting finding, not the noise.
# Backend
cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirement.txt
cp .env.example .env # optional, only needed for the AI features (see §6); add your GEMINI_API_KEY
python main.py
# Frontend (separate terminal)
cd frontend
npm install
cp .env.example .env # optional, only needed if the backend isn't on 127.0.0.1:8000
npm run devNo API keys needed for the four analytical lenses: yfinance, SEC EDGAR, and NREL's ATB data
lake are all unauthenticated. The AI features (methodology chatbot + live cross-lens narrative,
see §6) do need a free GEMINI_API_KEY in backend/.env; everything else works without it.
Backend tests (pure scoring/analysis logic, no network calls) live in backend/tests/:
cd backend
source venv/bin/activate
python -m pytestclimateTechAnalysis/
├── backend/
│ ├── api/
│ │ ├── analysis.py # /statistics, /fundamentals, /policy, /physical, /{vertical}/explain
│ │ ├── data.py # Chart endpoints for all four lenses
│ │ └── chat.py # /chat: methodology + live-data chatbot
│ ├── config/
│ │ ├── baskets.py # Pure-play equity baskets + GSPC/XLE benchmark tickers
│ │ └── policy_events.py # Curated US federal policy event table (now used only for the
│ │ # Market chart's event markers, see Policy lens note below)
│ ├── services/
│ │ ├── market/ # fetch.py (weekly price fetch + benchmark matrix, disk caching),
│ │ │ # analysis.py (RegimeAnalyticsEngine: ZIRP-vs-hiking comparison,
│ │ │ # descriptive), visualization.py (regime-comparison +
│ │ │ # cumulative-performance charts)
│ │ ├── fundamentals/ # fetch.py (SEC EDGAR client), analysis.py (leverage verdict + charts)
│ │ ├── policy/ # analysis.py (PolicyRegimeEngine: pre/post-IRA comparison, scored)
│ │ ├── physical/ # fetch.py (NREL ATB client), analysis.py (LCOE verdict + charts)
│ │ ├── chat/ # gemini_client.py (Gemini wrapper), context.py (shared grounding:
│ │ │ # static docs + live lens data), service.py (chatbot), explain.py
│ │ │ # (per-vertical narrative + cache), see §6
│ │ └── scoring.py # Shared Dependency Score (tanh) + significance-to-confidence math
│ ├── schemas/
│ │ ├── analysis.py # Pydantic response models for all four lenses
│ │ └── chat.py # Chat + explain request/response models
│ └── main.py
└── frontend/
└── src/ # Dashboard: 4 lens sections per card + consensus indicator,
# ChatWidget (floating methodology assistant) + ExplainPanel
# (per-vertical cross-lens narrative), see §6
Three lenses produce a scored Dependency Score; the Market lens is deliberately descriptive (see below for why). None of them rely on the others, and none get the final word. The dashboard shows all four side by side with a consensus indicator built from the three scored lenses, rather than collapsing everything into one number.
| Lens | Question it asks | Method | Scored? | Source |
|---|---|---|---|---|
| Market | How has a vertical's performance vs. the S&P 500 shifted between the ZIRP era (2009-2021) and the hiking/higher-for-longer era (2022-present)? | Regime comparison of weekly relative-to-GSPC returns | No, descriptive context | Yahoo Finance |
| Fundamentals | Can the company cover its own debt without help? | Median interest coverage & Debt/EBITDA from actual filings | Yes | SEC EDGAR (XBRL) |
| Policy | Has a vertical's performance vs. the S&P 500 shifted since the IRA's signing (2022-08-16)? | Regime comparison of weekly relative-to-GSPC returns, split pre/post-IRA | Yes | Yahoo Finance |
| Physical | Does the technology's own cost of energy depend on cheap financing? | LCOE sensitivity to WACC ±200bps, using NREL's own published cost/performance assumptions | Yes | NREL Annual Technology Baseline |
Full methodology, data-source rationale, and the verified bugs/fixes that shaped this design are
in docs/DATA_SOURCING_PLAN.md and
docs/REVIEW_AND_ROADMAP.md, including a documented case where the
original single-ETF, single-regression version of this engine had a data-fabrication bug that
mechanically biased every verdict toward "durable," and a later case where a whole-period HAC
regression turned out to be hiding decades of regime change (Grid's rolling beta) behind one
misleading static coefficient. Both are the reason this project moved away from static regressions
toward the regime-comparison design described below.
Why Market is descriptive but Policy isn't, even though both compare performance across two regimes relative to GSPC: verified directly against real data (not assumed) that Market's ZIRP-vs-hiking delta doesn't clear a genuine significance test for any vertical, at any data frequency tried (weekly or monthly). The large-looking annualized deltas don't survive contact with how volatile these stocks actually are, given the available sample sizes. Policy's pre/post-IRA split uses the same mechanics but happens to produce one real, robust result (Solar, p=0.093 weekly / p=0.061 monthly, checked both, consistent), so it keeps the project's standard significance-gated Dependency Score. Forcing Market's data through that same machinery would just make every vertical "Inconclusive," which defeats the purpose. So it's reported as honest, caveated context instead of a confidence score the data doesn't support.
Note on verdicts: each scored lens's verdict is a continuous Dependency Score (−100 fully
durable to +100 fully dependent, see backend/services/scoring.py), derived from a documented
threshold (e.g. Policy's 10-percentage-point/year regime-delta threshold, roughly the market's own
long-run equity risk premium) graded via tanh instead of a hard cliff, discounted by a genuine
significance test where one exists. Scores within ±20 of zero render as Inconclusive rather
than being forced into a binary label, a real correction, not cosmetic: forcing every
non-significant result to a confident-sounding label is a failure to reject the null, not evidence
of durability.
Not every lens applies to every vertical, and that's reported explicitly rather than hidden: Grid has no Physical-lens entry (NREL's ATB doesn't model grid/T&D infrastructure on an LCOE basis), and Storage has no Physical-lens entry either (ATB only publishes CAPEX-side costs for batteries, no capacity factor, WACC, or LCOE, since storage doesn't "generate" energy the way solar/wind do).
This engine exists to test a specific thesis, stated and scored like an experiment rather than
narrated after the fact. Full per-vertical reasoning lives in
docs/CASE_STUDY_MEMO.md; this section is the condensed version.
Thesis under test: government subsidies via the IRA and global net-zero commitments are channeling trillions into solar, wind, battery storage, and grid modernization. Which of these verticals have durable economics, and which are purely subsidy-dependent? (Scope note: a deliberate US-listed-equity case study within that broader global trend, see Limitations.)
Position: Solar and Battery Storage have durable, self-sustaining economics. Wind and Grid Modernization are capital-heavy and structurally more dependent on favorable financing conditions.
- Solar: durable on operating economics (no fuel input, steep cost-decline curve, diversifying demand).
- Storage: durable on a trajectory argument (falling lithium-ion costs), but flagged in advance as the vertical most likely to show strained near-term financials regardless of that long-run direction.
- Wind: capital-heavy by nature; expected real WACC sensitivity at both the project and company level.
- Grid: flagged upfront as the one vertical where "capital heavy" and "financing dependent" could decouple. This data covers equipment/EPC suppliers, not the asset-owning utilities that actually bear financing risk.
Falsifiable predictions, stated in advance: confirms the thesis if Solar/Storage show strong coverage and low leverage while Wind/Grid show elevated leverage and high WACC-sensitivity; contradicts it if that pattern reverses; complicates it (a real outcome, not a failure) if durability diverges between company-level and project-level economics for the same vertical.
Partially, and unevenly across mechanisms. That unevenness is itself the finding.
| Vertical | Prior view | Company-level (Fundamentals) | Technology/price-based (Physical, Policy, Market) | Outcome |
|---|---|---|---|---|
| Solar | Durable | Confirms | Contradicts (2 of 3 secondary reads) | Complicated |
| Storage | Durable | Contradicts, unambiguously | Inconclusive / consistent-but-not-significant | Contradicted |
| Wind | Dependent | Confirms | Confirms (where measurable) | Confirmed |
| Grid | Dependent | Contradicts | Inconclusive / mildly durable-leaning | Contradicted (as anticipated) |
"Durable vs. subsidy-dependent" turns out not to be a property of a vertical, but of a company's position in that vertical's value chain and which economic layer you're measuring (balance sheet vs. project economics vs. market pricing). Solar and Grid make the point most directly: Solar's companies are solvent while its project economics and post-IRA pricing both lean dependent; Grid's companies look durable specifically because the basket is suppliers, not the capital-intensive asset owners the "grid modernization is capital-heavy" intuition is really about. Storage's negative interest coverage needs no statistical inference at all: a plain balance-sheet fact, and the single strongest finding in the whole analysis. Per-vertical numbers behind this table are in §5 below.
- US-listed scope. The baskets are US-domiciled companies (with 2 non-US exceptions currently
live in the data:
DNNGY/Denmark in Wind,NVT/Ireland in Grid; seedocs/REVIEW_AND_ROADMAP.mdPhase 7). Makes a global theme tractable, but excludes genuine global supply-chain names (Vestas for wind, in particular). - Small baskets. Storage (3 tickers) and Wind (3 tickers) are thin; a single constituent's idiosyncratic move can matter more than in Grid's 6-7-name basket.
- Market has no scored verdict anywhere (see §3 for why). Its descriptive reads are directionally informative, not confirmed findings.
- Policy's one significant result (Solar) is confounded by design. A multi-year pre/post split can't fully separate an IRA-specific effect from the concurrent rate-hiking cycle and sector-specific competitive dynamics (Chinese panel oversupply).
- Physical has no entry for Grid or Storage (data-availability gap in NREL's ATB, see §3), so those two verticals lean more heavily on Fundamentals.
The numbers behind §4's verdict table. Dependency Score in parentheses for scored lenses (−100 fully durable to +100 fully dependent; |score| ≤ 20 reads as Inconclusive). Market has no score; its column reports the raw regime read.
| Vertical | Market (context) | Fundamentals | Policy | Physical |
|---|---|---|---|---|
| Solar | Edge reversed in high-rate era (ZIRP +5.5pp/yr → hiking −10.1pp/yr, not significant) | Self-sufficient (−74.7) | Post-IRA Underperformance (+33.9, p=0.093) | Capital-Intensity Dependent (+59.8) |
| Storage | Edge reversed in high-rate era (ZIRP +44.9pp/yr → hiking −12.6pp/yr, not significant) | Financing Dependent (+100.0, ICR −9.1x) | Inconclusive (0.0, p=0.86) | N/A, no LCOE concept applies |
| Wind | No material regime shift (weak in both eras) | Financing Dependent (+34.7, ICR 1.5x, D/E 5.4x) | Inconclusive (0.0, p=0.22) | Capital-Intensity Dependent (+56.3) |
| Grid | No material regime shift (flat ZIRP, modest hiking-era edge) | Self-sufficient (−64.0) | Inconclusive (0.0, p=0.58) | N/A, no LCOE concept applies |
These numbers move whenever the underlying data does: market prices update continuously, SEC filings update quarterly, NREL's ATB updates annually. Use the dashboard's refresh button for the current live values rather than treating this table as a snapshot in time.
Two Gemini-powered features layer natural-language output on top of the four lenses. The goal isn't just convenience, it's accessibility and transparency: making the methodology and current results legible to someone who doesn't want to read six markdown files, without asking that person to trust an opaque AI-generated verdict in exchange. Every answer is grounded in content a reader could just as easily read themselves, and both features are built to say "not covered by this data" rather than fill a gap with something that merely sounds plausible.
| Feature | What it does | Endpoint |
|---|---|---|
| Methodology Chatbot | Free-form Q&A about how the engine works and what its current results mean, grounded in both the static methodology docs and the four lenses' live output, not a frozen snapshot | POST /api/v1/chat |
| Explain This Disagreement | One click on a vertical's card generates a short narrative (the live equivalent of a docs/CASE_STUDY_MEMO.md entry) naming where that vertical's lenses currently agree or disagree |
GET /api/v1/analysis/{vertical}/explain |
How the transparency goal is enforced, not just claimed: both features share the same
grounding context (backend/services/chat/context.py): the full methodology-doc corpus (small
enough to include in full on every request, no embeddings/vector DB, so nothing is silently
dropped or approximated) plus a live snapshot of what each lens currently returns, pulled from the
same cached engines the dashboard's own charts use. The system prompt requires every claim to cite
which lens it comes from, forbids asserting causality beyond what the docs already claim (e.g.
"Post-IRA underperformance," never "caused by the IRA," the same restraint this project applies
to itself elsewhere), and instructs the model to say plainly when a question falls outside the
provided context instead of guessing. Neither feature computes a new verdict or score. They
describe the existing ones, they don't grade them, so nothing an AI generates can quietly become a
source of truth the numeric lenses didn't already produce.
The narrative endpoint additionally caches its output in-memory per (vertical, current score signature), so repeat requests don't re-trigger a Gemini call unless the underlying lens numbers
actually changed. The narrative shown always matches the numbers on the card next to it, never a
stale generation from an earlier state.
Setup: requires a free GEMINI_API_KEY in backend/.env (see backend/.env.example). This
is the only feature in this project that needs an API key; everything else works without one. Get
a key at aistudio.google.com/apikey. GEMINI_MODEL
defaults to gemini-flash-latest, a Google-maintained alias that tracks their current flash model
rather than a dated snapshot that can be retired mid-project (as gemini-2.5-flash was, here).
- Data:
yfinance(equities, GSPC, XLE), SEC EDGAR (XBRL filings), NREL ATB (technology cost/performance data) - Analysis:
pandas,scipy(Welch's t-tests for the regime comparisons),pyarrow(NREL Parquet data) - AI:
google-genai(Gemini API): methodology chatbot + live cross-lens narrative (see §6) - Backend:
FastAPI+Uvicorn, with on-disk caching so repeat requests don't re-hit external APIs - Frontend:
Vite+React+TypeScript+Tailwind CSS v4+Lucide React - Charts:
react-plotly.js, rendering Plotly figures generated server-side per lens