status: available for work

Valtteri Murtomäki

Quantitative Analyst — Risk Modelling

I build statistical models that answer decisions, not just questions: what a price change really costs in sales, how wrong a demand forecast could be, how bad a bad day on electricity prices gets. Each one ships as a typed, tested, containerized system — not a notebook.

Statistician (M.Sc., Turku 2026). Previously ran regulated cold-chain logistics — where the habit of building systems that fail loudly came from.

Statistics & Modelling

Time Series & Volatility Modelling (GARCH) Extreme Value Theory Causal Inference (Double ML) Conformal Prediction Model Validation & Backtesting Bayesian Inference

Languages & Tools

Python SQL / T-SQL R Pandas / NumPy / SciPy Scikit-learn LightGBM Streamlit / Plotly

Cloud & Engineering

Azure SQL Terraform Docker GitHub Actions (OIDC) Pytest MyPy (strict) / Ruff uv
// case studies

Selected Work

Four projects spanning tail-risk forecasting, causal inference, prediction intervals, and regulatory reporting measurement.

P01

GARCH-EVT Risk Engine — Scheduled Azure Deployment

Azure SQLTerraform GitHub Actions OIDCPandera archSciPy

Runs unattended every day to answer one question for anyone exposed to Finnish electricity prices — how bad could a bad day get — and halts rather than publishing when the input series has a gap it cannot defensibly fill or the volatility model fails to converge.

The first out-of-sample backtest rejected 99% coverage — 8 breaches against 2.44 expected, Kupiec p = 0.0047 — and the forecast was withheld. On the current pinned range: 14 breaches against 14.05 expected at 95% over 281 days (Kupiec p = 0.99), with the 99% level reported alongside its power caveat rather than as confirmation.

  • That fix changed four things at once (threshold selector, rolling window, exceedance floor, evaluation period). A later ablation cleared the selector and the floor; window length and evaluation period remain confounded, and cannot be separated because the 2023 range no longer exists upstream. The original single-cause attribution was wrong, and its replacement is narrower rather than conclusive. Both the failure and the correction are in the devlog.
  • Rebuilt a local research pipeline as a passwordless, zero-secret Azure deployment: Terraform-provisioned Azure SQL Serverless, Blob-backed remote state, GitHub Actions authenticated via OIDC federated credentials.
  • Built an idempotent ingestion layer (staging table + T-SQL MERGE) with Pandera contracts enforcing a gapless, timezone-aware hourly index, and exponential backoff to survive Azure SQL Serverless cold starts (error 40613).
  • Fitted a rolling GJR-GARCH(1,1,1) + Peaks-Over-Threshold EVT engine with daily Kupiec POF and Christoffersen conditional-coverage tests, plus a raw Acerbi–Székely ES statistic that is reported rather than thresholded (it follows no standard distribution); the pipeline halts on unfillable data gaps or convergence failures rather than silently dropping days.
  • Benchmarked against filtered historical simulation, a GARCH-t quantile, and an unconditional empirical quantile over an identical 281-day scheme. No arm separates on coverage — including the unfiltered one — so the honest reading is that the sample lacks the power to rank them; GARCH-EVT differs only in running 10–15% more conservative at 99%.
  • Fixed three Azure/ODBC/SQLAlchemy production issues: OIDC subject-claim mismatch, driver auth keyword collision, SQL type inference on tz-aware timestamps.
P02

Causal Pricing Engine

View repo
PythonDoubleML LightGBMStreamlit uvPytestMyPy

Tells a pricing team how much of their brand's share a price change would actually move, separating real customer response from the brand loyalty and competitor discounting that make raw sales data misleading — and lets them test a scenario in a dashboard before committing.

A 10-cent increase in sale price lowers the probability of choosing the brand by ~4.6 percentage points (θ̂₀ = −0.460, p < 0.001), holding loyalty and competitor price and promotion fixed. Estimated on a linear probability scale, so this is a local slope, not a calibrated probability model.

  • Read as a local slope, not a dollar-scale prediction: the treatment (sale price) spans $1.39–$2.09, so a full $1.00 move is outside the support of the data.
  • Omitted-variable-bias sensitivity puts the robustness value at 12.3%: at worst-case alignment, an unobserved confounder would need to explain ~12.3% of residual variance in both treatment and outcome to null the point estimate — 8.1% to null significance. Not yet benchmarked against the observed covariates, so it bounds fragility without establishing that no such confounder exists.
  • Observations cluster by store and week but the model was fitted with i.i.d. standard errors, so the p-value is understated; the point estimate is the defensible quantity.
  • Estimated the effect with a partially linear DML model on the partialling-out score — LightGBM nuisance learners, 5 folds × 5 repetitions. PriceCH and SpecialCH are excluded together: sale price is list price minus discount, and the discount is near-zero absent a promotion, so conditioning on both leaves almost no residual treatment variation. The cost is that list-price level goes uncontrolled — a stated trade-off, not a free lunch.
  • Validated the estimator against a synthetic design with known θ₀ and a second with an analytically derived omitted-variable bias, both asserted in CI alongside Ruff, MyPy (strict), and a locked dependency graph.
  • Built a typed src/ layout with a Streamlit simulator that reads the pipeline artifact rather than refitting.
P03

Adaptive Conformal Forecasting

Distribution-free prediction intervals for volatile time series
View repo
PythonMAPIE DockerPlotly uvPytest

Turns a single-number electricity demand forecast into a 90% prediction interval — the band the outcome falls inside nine hours in ten, not a best case or a worst case — that planners can size capacity and hedging decisions against. The band recalibrates itself against realized coverage as the series shifts regime, instead of holding a width that stopped being correct.

On 1,500 held-out hours at a 90% target, the adaptive layer covered 89.9% with intervals 7.5% narrower than a static conformal baseline that over-covered at 92.7%.

  • Reported with its resolution: at n = 1,500 the binomial SE on coverage is ≈0.8 pp before correcting for serially dependent misses, so 89.9% is indistinguishable from the 90% target — the separation from 92.7% is the informative comparison, not the last decimal.
  • The γ sweep behind that default was run on the same held-out window the numbers are reported on, so the width reduction is optimistically biased; a three-way split is the first structural fix. γ = 0.005 gave a larger reduction (10.9%) than the chosen 0.01, so the default is a judgment call on adaptation speed rather than an optimum the sweep establishes.
  • Above γ ≈ 0.03 the update overshoots after miscoverage events — coverage still tracks 90%, but mean width inflates sharply (47% wider than static at γ = 0.05).
  • Applied EnbPI with block-bootstrap residuals (MAPIE) in place of split conformal — a weaker assumption, not no assumption: coverage becomes approximate and asymptotic under stationarity and mixing conditions rather than finite-sample under exchangeability.
  • Fixed an ACI update that ran once per 168-hour chunk on the chunk's aggregate error rate instead of per timestep — roughly two orders of magnitude coarser than the Gibbs & Candès recursion. The α path is now per-step; the emitted band stays piecewise-constant over the batching interval, so the displayed interval is a downsampled view of it.
  • Containerized the stack with Docker and wrote one end-to-end validation script covering install, tests, lint, image build, and container health check.
P04

ESEF Machine-Readability Benchmark

PythonSQLite XBRL / iXBRLCluster bootstrap uvPytestMyPy (strict)

EU-listed companies are required to file their annual accounts in a machine-readable format, so software should be able to find the headline figures — revenue, profit, assets, equity, liabilities, cash — without guessing. This measures how often it actually can, on every Finnish issuer that filed.

93.9% of 882 issuer-field cells carry the ESEF core standard element (95% cluster-bootstrap CI 91.8%–95.7%, 147 issuer clusters, B = 10,000). The gap is not spread evenly: liabilities is tagged in 79.6% of filings and revenue in 89.8%, while the other four sit above 98%.

  • The dominant cause is legitimate presentation, not defective tagging: 30 of 147 issuers report equity and liabilities split into non-current and current components, with no bare total line to tag. That is a valid IAS 1 statement and a machine-readability gap at the same time, which is the finding.
  • Excluded cells stay in the denominator. Conditioning on classifiability would have defined away part of the thing being measured; the exclusions are published by reason code so a reader can apply their own test.
  • The pre-registered reason for clustering on the issuer was falsified by the data and is published as falsified: shared templates and tagging vendors predict an intraclass correlation near 1, and the measured value is 0.1099. The clustering is still correct; the argument for it was not.
  • Stopped the planned LLM extraction arm on a measurement, before a single document was sent: the deepest lossless redaction still left a mean 1,331,978-token document against a 1,050,000-token context window, at $51.66 projected for this frame. Recorded VOID rather than FAIL — a gate whose inputs were never observed has no verdict — and the scope change is a dated amendment, not a rewritten spec.
  • Removed a verification script that ran the scripts writing its own inputs and then asserted PASS on them, and traced a one-character constant corruption that disabled the entire redactor while mypy --strict reported success. Three reasoned diagnoses were wrong; printing the bytes at the failing offset settled it.