55 <br />
66 A sparse index replication engine that compresses broad benchmarks into compact,
77 tradable portfolios using a custom ADMM solver, real backtests, a FastAPI backend,
8- and a polished Next.js product surface .
8+ and an interactive Next.js frontend .
99</p >
1010
1111<p align =" center " >
4141<p align =" center " >
4242 <a href =" https://sparse-index-tracker.vercel.app " >
4343 <img
44- alt="Sparse Index Tracker preview"
45- src="https://img.shields.io/badge/Live%20Demo-Open%20the%20quant%20terminal-111827?style=for-the-badge "
44+ alt="Sparse Index Tracker frontend preview"
45+ src="docs/images/frontend/landing.png "
4646 />
4747 </a >
4848</p >
@@ -62,14 +62,14 @@ Sparse Index Tracker learns a compact basket of stocks that tracks a broad bench
6262like the S&P 500. It does that with an L1-regularized optimization problem and a custom
6363ADMM solver built specifically for sparse portfolio replication.
6464
65- The result is a full-stack quant system : research pipeline, solver, validation suite ,
66- API, cache, cloud deployment, and frontend.
65+ The result connects the pieces that usually stay separate : research pipeline, solver,
66+ validation suite, API, cache, cloud deployment, and frontend.
6767
6868It is designed to be read in layers:
6969
7070| If you are... | Start here | What you will see |
7171| --- | --- | --- |
72- | A recruiter or engineering reviewer | [ Live Demo] ( https://sparse-index-tracker.vercel.app ) | A complete product, not a notebook screenshot |
72+ | A recruiter or engineering reviewer | [ Live Demo] ( https://sparse-index-tracker.vercel.app ) | The deployed interface and live endpoints |
7373| A quant researcher | [ Research Lab] ( https://sparse-index-tracker.vercel.app/research ) | Regularization paths, convergence, stress regimes |
7474| A backend engineer | [ ` src/sit/api ` ] ( src/sit/api ) | FastAPI, Pydantic v2, caching, rate limits, deployment hardening |
7575| A numerical optimization reviewer | [ ` src/sit/solvers ` ] ( src/sit/solvers ) | ADMM solver internals and sparse optimization logic |
@@ -119,8 +119,23 @@ research pipeline.
119119| Supported markets | 4 | S&P 500, Nasdaq-100, Russell 2000, Nifty 50 |
120120| Test suite | 274 pytest tests | Backend/research validation coverage |
121121
122- The important claim is not just that the model works. It is that the model is
123- wrapped in enough engineering to be inspected, deployed, tested, and used.
122+ The result is evaluated both numerically and operationally: solver agreement tests,
123+ walk-forward validation, regime slices, API tests, and frontend build checks all sit
124+ in the same repository.
125+
126+ <p align =" center " >
127+ <img
128+ alt="Sparsity versus out-of-sample tracking error Pareto frontier"
129+ src="plots/sparsity_vs_te_pareto.png"
130+ />
131+ </p >
132+
133+ <p align =" center " >
134+ <sub >
135+ Sparsity is controlled by the regularization path. Moving along the curve trades
136+ a smaller portfolio for higher out-of-sample tracking error.
137+ </sub >
138+ </p >
124139
125140---
126141
@@ -191,41 +206,112 @@ flowchart LR
191206
192207## Mathematical Core
193208
194- The optimization problem is a sparse tracking problem:
209+ Let ` X ` be a matrix of constituent returns with shape ` T x N ` , where ` T ` is the
210+ number of training days and ` N ` is the number of stocks in the universe. Let ` y ` be
211+ the benchmark return vector over the same dates. The goal is to learn weights ` w `
212+ so that ` Xw ` behaves like ` y ` , while most entries of ` w ` become zero.
213+
214+ The base problem is:
195215
196216``` text
197- minimize tracking_loss(w) + lambda * sparsity_penalty(w)
198- subject to portfolio constraints on w
217+ minimize_w 1/2 ||Xw - y||_2^2 + lambda ||w||_1
218+ subject to w >= 0
199219```
200220
201- In plain English:
221+ After convergence, the positive weights are normalized back onto the fully invested
222+ simplex so they can be interpreted as portfolio weights:
223+
224+ ``` text
225+ w_i >= 0, sum_i w_i = 1
226+ ```
227+
228+ In plain language:
202229
203230- match the benchmark return stream,
204- - keep the active weight vector small and interpretable,
205- - make the solution fast enough to retrain,
206- - and expose the result as a usable product.
231+ - penalize portfolios that need too many names,
232+ - keep the final allocation long-only,
233+ - and return weights that can be converted into actual share counts.
234+
235+ ### Why L1 Creates Sparsity
236+
237+ The L1 term ` lambda ||w||_1 ` adds a cost for keeping weights alive. As ` lambda `
238+ increases, small marginal positions are pushed to exactly zero. This creates a
239+ regularization path:
240+
241+ ``` text
242+ low lambda -> more stocks, lower tracking error
243+ high lambda -> fewer stocks, higher tracking error
244+ ```
245+
246+ The Pareto plot above is the practical version of that statement: it shows how many
247+ active stocks the model keeps at different regularization strengths and what that
248+ does to out-of-sample tracking error.
249+
250+ ### Why ADMM Fits The Problem
207251
208252ADMM is a natural fit because it splits the problem into pieces that are easier to
209- solve:
253+ solve. The implementation introduces an auxiliary variable ` z ` and enforces ` w = z ` :
254+
255+ ``` text
256+ minimize 1/2 ||Xw - y||_2^2 + lambda ||z||_1 + I(z >= 0)
257+ subject to w - z = 0
258+ ```
259+
260+ This gives three interpretable update steps:
210261
211262| ADMM component | Role in this project |
212263| --- | --- |
213- | Weight update | Solves the smooth tracking objective efficiently |
214- | Sparse step | Applies soft-thresholding to encourage fewer active holdings |
215- | Dual update | Keeps the split variables consistent |
216- | Adaptive rho | Stabilizes convergence across different universes |
217- | Residual checks | Provides transparent stopping diagnostics |
264+ | ` w ` update | Solves a ridge-like least-squares system |
265+ | ` z ` update | Applies positive soft-thresholding, which creates sparsity |
266+ | ` u ` update | Updates the scaled dual variable so ` w ` and ` z ` agree |
267+ | Adaptive rho | Rebalances primal and dual progress across different data scales |
268+ | Residual checks | Stops only when primal and dual feasibility are both small |
269+
270+ The expensive matrix solve is stabilized with a Cholesky factorization of
271+ ` X'X + rho I ` . When ` rho ` changes, the factorization is recomputed; otherwise the
272+ cached factor is reused.
273+
274+ <p align =" center " >
275+ <img
276+ alt="Eight-regime stress test summary"
277+ src="plots/regime_summary.png"
278+ />
279+ </p >
280+
281+ The regime summary is important because it checks the model against different market
282+ personalities instead of relying on one aggregate backtest. A sparse tracker can look
283+ good in calm markets and fail when correlations, volatility, or leadership change.
284+ Here the same modeling approach is evaluated across crash, volatile, bull, and stable
285+ windows, with each row reporting active holdings, out-of-sample tracking error,
286+ correlation, and test R2. That makes robustness visible: the model is not only
287+ matching one historical curve, it is being stress-tested across the kinds of periods
288+ where index replication usually breaks.
289+
290+ ### How The Math Was Checked
291+
292+ The mathematical implementation is tested from several angles:
218293
219- The implementation emphasizes readability and reproducibility over hiding everything
220- inside a solver call.
294+ | Check | What it verifies |
295+ | --- | --- |
296+ | Synthetic sparse recovery | On controlled problems, the recovered support and weights match the planted sparse portfolio |
297+ | Lambda-max behavior | Above ` lambda_max ` , the solver correctly collapses to the zero solution before normalization |
298+ | Objective trajectory | The recorded objective ends below its starting value |
299+ | CVXPY agreement | ADMM and CVXPY solve the same convex objective to nearly the same minimizer |
300+ | LASSO agreement | The sklearn LASSO baseline agrees with ADMM after matching the lambda scaling |
301+ | Simplex checks | Returned portfolio weights are non-negative and normalized |
302+ | Walk-forward tests | Rebalanced weights remain valid through the historical simulation |
303+ | Regime tests | Performance is sliced across distinct market conditions rather than only one full-sample number |
304+
305+ The solver is therefore checked at the mathematical level, the backtest level, and
306+ the API/product level.
221307
222308---
223309
224310## Why Not Just Use CVXPY?
225311
226312CVXPY is excellent for modeling. This project still uses solver baselines for
227- comparison, but implements a custom ADMM path because the point is to own the full
228- optimization stack .
313+ comparison, but implements a custom ADMM path so the optimization steps, convergence
314+ diagnostics, and live retraining behavior are visible in the codebase .
229315
230316That gives the project:
231317
@@ -268,10 +354,10 @@ curl "https://sparse-index-tracker.vercel.app/api/proxy/api/v1/lambda-path?index
268354
269355---
270356
271- ## What Makes It Production-Shaped
357+ ## How The System Is Packaged
272358
273- This is intentionally built like a small production system rather than a single
274- research notebook .
359+ The repository keeps research, API, frontend, and deployment pieces together so each
360+ claim can be traced to code or an artifact .
275361
276362| Layer | What is included |
277363| --- | --- |
@@ -301,7 +387,7 @@ research notebook.
301387|-- frontend/ # Next.js product frontend
302388|-- deploy/ # Dockerfile and Azure deployment scripts
303389|-- docker-compose.yml # Local API + Redis stack
304- `-- PHASE6_AZURE_DEPLOYMENT_RUNBOOK .md
390+ `-- README .md
305391```
306392
307393Files worth reading first:
@@ -419,11 +505,9 @@ The live system is deployed as:
419505| Observability | Application Insights + Log Analytics |
420506| CI | GitHub Actions |
421507
422- The Azure path is documented in beginner-friendly detail here:
423-
424- ``` text
425- PHASE6_AZURE_DEPLOYMENT_RUNBOOK.md
426- ```
508+ Deployment scripts live under ` deploy/azure ` , while credentials and cloud-specific
509+ values are supplied through local environment files, Azure secrets, or GitHub
510+ Actions variables.
427511
428512---
429513
@@ -465,8 +549,8 @@ lock it down.
465549### Why do live runs sometimes take time?
466550
467551` /invest_live ` retrains from recent market data and fetches current prices. That is
468- more impressive than serving a static JSON file, but it depends on external data
469- providers and may take several seconds.
552+ different from serving a static JSON file: it depends on external data providers and
553+ may take several seconds.
470554
471555### Why sparse portfolios?
472556
0 commit comments