Financial Index Replication
Select a sparse 20-stock replication basket and weights that track an S&P 500-like benchmark.
What this template is for
Index funds and separately managed accounts often need to track a broad benchmark without holding every constituent. Full replication is operationally expensive, especially for smaller accounts, tax-aware portfolios, or products with custody and trading constraints. A sparse replicating basket captures most of the benchmark exposure while cutting the number of positions to trade and maintain.
The hard part is that name selection and weight sizing interact. The best 20 names are not simply the largest constituents or the highest-correlated stocks; they have to work together as a portfolio while respecting sector and per-name trading-capacity rules. This template builds that basket from a 50-stock, S&P 500-like universe: it selects exactly 20 names and their weights so the portfolio follows the benchmark’s historical returns as closely as possible.
It uses Prescriptive reasoning to co-optimize selection and sizing in a single mixed-integer program that minimizes tracking residuals subject to long-only weights, a maximum position size, sector neutrality, and per-name average-daily-volume (ADV) participation limits.
Who this is for
- Quantitative analysts building index replication workflows
- Portfolio managers exploring sparse benchmark tracking
- Data scientists learning mixed-integer optimization with financial constraints
- Engineers modeling linked selection and allocation decisions
What you’ll build
- A semantic model for stocks, sectors, benchmark returns, and stock returns
- A mixed-integer optimization model with 20-name cardinality
- Long-only portfolio weights with max position constraints
- Sector-neutrality and ADV participation constraints
- A tracking residual objective over historical returns
- Full-history tracking error reports and a simple baseline comparison
What’s included
financial_index_replication.py— Main script with the semantic model, optimization model, solve, and reportingrunbook.md— a paste-testable walkthrough that reproduces the template step by step with the RAI skills; as important a reference as the script itselfdata/stocks.csv— 50-stock universe with ticker, sector, benchmark weight, liquidity, and previous weightdata/index_returns.csv— Monthly S&P 500-like benchmark returnsdata/stock_returns.csv— Monthly historical returns by stockpyproject.toml— Python package configuration with dependencies
Prerequisites
Access
- A Snowflake account that has the RAI Native App installed.
- A Snowflake user with permissions to access the RAI Native App.
Tools
- Python >= 3.10
- RelationalAI Python SDK (
relationalai) == 1.0.14
Quickstart
-
Download ZIP:
Terminal window curl -O https://docs.relational.ai/templates/zips/v1/financial_index_replication.zipunzip financial_index_replication.zipcd financial_index_replication -
Create venv:
Terminal window python -m venv .venvsource .venv/bin/activatepython -m pip install --upgrade pip -
Install:
Terminal window python -m pip install . -
Configure:
Terminal window rai init -
Run:
Terminal window python financial_index_replication.py -
Expected output (a few lines confirm a successful run; exact figures depend on the data):
======================================================================FINANCIAL INDEX REPLICATION======================================================================Universe: 50 stocksSelected names: exactly 20Max position: 10%Sector active band: +/- 4%Status: OPTIMALObjective: total absolute residual = ...=== Selected Replication Basket ===ticker sector weight benchmark_weight previous_weight avg_dollar_volume...Wrote benchmark-vs-replica returns to: data/replica_returns.csvThe solve selects exactly 20 names, reports the basket, sector exposures, tracking quality, and a baseline comparison, and writes
data/replica_returns.csvfor plotting. The full printout is inrunbook.md.
Template structure
.├─ README.md # this file├─ runbook.md # step-by-step analyst walkthrough├─ pyproject.toml # dependencies├─ financial_index_replication.py # main entrypoint: model, solve, report└─ data/ ├─ stocks.csv # 50-stock universe ├─ index_returns.csv # benchmark monthly returns ├─ stock_returns.csv # per-stock monthly returns └─ replica_returns.csv # written by the script after solvingStart here: run python financial_index_replication.py for the full solve and reporting end to end, or follow runbook.md to reproduce it step by step with the RAI skills.
Sample data
The bundled data is synthetic but shaped like an S&P 500 replication problem, so the template runs without licensed market data.
data/stocks.csv(50 rows) — the investable universe:ticker,name,sector,benchmark_weight,avg_dollar_volume, andprevious_weight(the prior-period holding).data/index_returns.csv— monthly benchmark returns keyed bydate(index_return).data/stock_returns.csv— monthly per-stock returns keyed bydateandticker(return).data/replica_returns.csv— written by the script after solving, withdate,index_return, andreplica_returnfor downstream plotting.
The benchmark constituent weights were generated by first assigning broad target sector allocations, then drawing uneven positive stock weights within each sector and scaling each sector back to its target. The benchmark return each month is the weighted sum of all constituent returns, plus a small noise term so the sparse 20-name replication problem is realistic rather than perfectly mechanical.
Model overview
Four concepts describe the universe, its sector grouping, and the historical return panel; the decision variables attach to Stock and ReturnDate.
- Key entities:
Stock(a constituent in the investable universe),Sector(a grouping used for the neutrality constraint), andReturnDate(a month in the return panel). - Primary identifiers:
Stockbyticker;Sectorbysector_name;ReturnDatebydate. - Important invariants: exactly
N_REPLICATION_NAMESstocks are selected; weights are non-negative, at mostMAX_WEIGHTeach, and sum to 100%; a stock can carry weight only if selected; each sector’s weight stays withinSECTOR_ACTIVE_BANDof its benchmark weight.
For the full concept and property definitions, see financial_index_replication.py; runbook.md builds them step by step with the RAI skills.
How it works
The design patterns worth noting: cardinality-constrained selection (binary variables choose exactly 20 stocks), linked binary and continuous decisions (a stock carries weight only if selected), an L1 tracking objective (minimize absolute residuals to keep the problem linear with binary selection), sector neutrality (replicated sector exposure stays within a fixed active band), ADV participation control (each name’s buy or sell is capped as a fraction of average daily dollar volume), and a baseline comparison against an equal-weight top-correlation basket.
stocks + returns → selection + weight variables → tracking residuals → cardinality/sector/ADV constraints → MIP solve → basket + tracking report1. Define selection and weight variables
Each stock carries two linked decisions: a binary x_selected (in the basket or not) and a continuous x_weight (portfolio weight). A linking constraint holds weight <= MAX_WEIGHT * selected, so an unselected name is forced to zero weight.
2. Match benchmark returns
For each historical month, the model creates positive and negative residual variables that absorb the gap between the benchmark return and the replica’s weighted return:
index_return[t] - sum_i weight[i] * stock_return[i,t] = pos_error[t] - neg_error[t]The objective minimizes the total absolute residual:
minimize sum_t pos_error[t] + neg_error[t]This L1 formulation keeps the problem linear and mixed-integer with the binary selection variables; after solving, the script computes the standard RMS tracking error across the full history. A classic L2 objective (squared residuals) is also a natural tracking-error formulation if the selected solver supports the resulting mixed-integer quadratic problem.
3. Add portfolio realism
The template layers on the constraints practitioners expect: exactly 20 selected stocks; weights sum to 100%; no shorting; max 10% per selected stock; sector weights within +/- 4% of benchmark sector weights; and per-name buy and sell amounts no more than 5% of average daily dollar volume (ADV).
4. Evaluate the portfolio
After solving, the script reports the selected names and weights, sector exposures and active sector weights, annualized tracking error, mean absolute residual, implied turnover, and a comparison to a simple top-correlation baseline. It also writes data/replica_returns.csv (date, index_return, replica_return) so you can plot the benchmark series against the optimized replica.
See financial_index_replication.py for the implementation and runbook.md for the skill-driven reproduction.
Customize this template
Use your own data
- Replace the synthetic CSVs with real benchmark and constituent returns if your data license allows it. Keep the headers:
ticker,sector,benchmark_weight,avg_dollar_volume,previous_weightforstocks.csv;date,index_returnforindex_returns.csv;date,ticker,returnforstock_returns.csv. - Make sure all three files cover the same date range and use the same return convention (simple or log); a mismatch silently drops months from the join or inflates residuals.
Tune parameters
N_REPLICATION_NAMESsets how many names the basket holds.MAX_WEIGHTtightens or relaxes the largest allowed position size.SECTOR_ACTIVE_BANDcontrols sector neutrality; tighten it for stricter tracking to benchmark sector weights.MAX_ADV_PARTICIPATIONsets per-name trading capacity; lower it for stricter ADV limits.
Extend the model
- Swap the L1 tracking objective for an L2 (squared-residual) objective if your solver supports the resulting mixed-integer quadratic problem.
- Add constraints practitioners expect, such as a turnover cap against
previous_weightor a minimum position size for selected names.
Scale up / productionize
- Point the
pd.read_csvcalls at Snowflake tables viamodel.data(...)to run over a larger universe and longer return history. - Pin dependencies and fix any random seed in the data-generation step for reproducible baskets across runs.
Troubleshooting
Why is the solver returning INFEASIBLE?
- The combination of `N_REPLICATION_NAMES`, `SECTOR_ACTIVE_BAND`, and `MAX_ADV_PARTICIPATION` may be over-constrained for the universe. Loosen the sector band first (e.g., 0.04 -> 0.06) and re-run.- `MAX_ADV_PARTICIPATION` interacts with `PORTFOLIO_VALUE` and `previous_weight`. A large portfolio rebalancing into low-ADV names can be infeasible -- raise the ADV cap, lower portfolio value, or expand the universe.- Check that `MAX_WEIGHT * N_REPLICATION_NAMES >= 1.0` so the full-investment constraint is reachable.Why does rai init fail or hang?
- Confirm the RAI Native App is installed in your Snowflake account and your user has access.- Check that your active Snowflake profile points to the right account/role; re-run `rai init` to refresh credentials.- Network proxies and corporate firewalls can block the auth handshake -- try from an unrestricted network.Why are my tracking-error numbers worse than the baseline?
- Verify that all three CSVs cover the same date range. A mismatch causes the script to silently drop months from the join.- Confirm `index_returns.csv` and `stock_returns.csv` use the same return convention (simple vs log) -- mixing them inflates residuals.- Inspect the selected basket's sector exposure: a tight `SECTOR_ACTIVE_BAND` can push the optimizer away from the highest-correlation names.Why did pd.read_csv fail on one of the data files?
- Confirm the file exists under `data/` and matches the expected headers (`ticker`, `sector`, `benchmark_weight`, `avg_dollar_volume`, `previous_weight` for stocks; `date,index_return` for index; `date,ticker,return` for stock returns).- Re-extract the template ZIP if any file looks truncated.- On Windows, ensure files are UTF-8 encoded with no BOM.Learn more
Core concepts
- Prescriptive reasoning — the
ProblemAPI, decision variables, constraints, and objectives used to co-optimize selection and weights. - Mixed-integer optimization — binary selection variables linked to continuous weights, and the cardinality constraint.
Language / modeling reference
- PyRel v1 language — concepts, properties, and relationships as used to model stocks, sectors, and the return panel.
CLI / SDK guides
rai initand configuration — connecting the template to your Snowflake-backed RAI account.
Support
- File issues at the RelationalAI templates repository.