Optimization
Submit portfolio optimization jobs for Indian equities or open-ended equity mutual funds using a variety of classical and modern allocation strategies.
Overview
The optimization endpoint accepts either a list of Indian equity tickers (NSE/BSE, via the stocks field) or a list of mutual fund scheme codes (via the funds field, currently in beta) along with one or more optimization methods, queues the job for asynchronous processing, and returns a run_id that you can use to track progress. Once the job completes, results are available as downloadable artifacts including charts, Parquet data files, Excel reports, and PDF reports.
Stocks and funds cannot be mixed in the same request. See the Mutual Funds (Beta) section for the MF schema and constraints.
The risk-free rate is automatically sourced from India's 10-year government bond yield (via Stooq) for the analysis period — you do not need to supply it. If the yield data is unavailable, a 2% annual default is used.
All optimization requests are processed asynchronously. The API immediately returns a 202 Accepted response with a unique run identifier. Use the Jobs API to poll for completion and retrieve results.
/jobsSubmit a new portfolio optimization job for asynchronous processing.
Authentication
Required — JWT Bearer token or API Key in the Authorization header.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| stocks | Array<object> | Required if no funds | Array of stock objects. Each object has ticker (string symbol, e.g. "RELIANCE" or "AAPL") and exchange (string, "NSE", "BSE" or "US"for NYSE/NASDAQ listings). Minimum 2 stocks. A request must stay within one market - US and Indian stocks can't be mixed, and a US portfolio requires a US benchmark plus risk_free.region: "US". Mutually exclusive with funds. |
| funds Beta | Array<object> | Required if no stocks | Array of mutual fund objects. Each object has scheme_code (positive integer, MFAPI scheme code) and an optional label (string, used in reports / PDFs). Minimum 2 schemes. Each scheme code is validated against the MFAPI master list and the AMFI equity-only category allow-list. Mutually exclusive with stocks. See the Mutual Funds (Beta) section below. |
| methods | Array<string> | Default: ["MVO"] | One or more optimization method enums. See Supported Methods below for the full list of 21 strategies. |
| benchmark | string | Default: "nifty" | Benchmark index for relative metrics (alpha, beta, tracking error, IR, capture ratios). One of 17 values (14 Indian + 3 US) - see Supported Benchmarks below. Benchmarks other than "nifty", "sensex", and "bank_nifty" have shorter history; the optimizer truncates the asset price series to align with the benchmark window. "sensex" is rejected for fund runs. |
| cla_method | string | Optional | Sub-method for Critical Line Algorithm. One of "MVO", "MinVol", "Both". Only used when "CriticalLineAlgorithm" is in methods. |
| rolling_backtest | object | Optional | Configuration for rolling walk-forward backtesting. When enabled: true, the job runs a time-series cross-validation over the full history instead of a single in-sample fit. See the Rolling Backtest section below for sub-fields. |
| risk_free | object | Default: India | Risk-free rate source. region is "IN" (default, Indian 10Y G-Sec via Stooq) or "US". For "US", supply a tenor - "FedFunds" (overnight effective fed funds rate) or a Treasury constant-maturity tenor ("1M", "3M", "6M", "1Y" … "30Y") - sourced from FRED. Required to be "US" for US portfolios. |
Request Example - Standard Optimization
{
"stocks": [
{ "ticker": "RELIANCE", "exchange": "NSE" },
{ "ticker": "TCS", "exchange": "NSE" },
{ "ticker": "HDFCBANK", "exchange": "NSE" },
{ "ticker": "INFY", "exchange": "NSE" },
{ "ticker": "ICICIBANK", "exchange": "NSE" }
],
"methods": ["MVO", "MinVol", "HRP", "BlackLitterman"],
"benchmark": "nifty"
}Request Example - US Stocks
{
"stocks": [
{ "ticker": "AAPL", "exchange": "US" },
{ "ticker": "MSFT", "exchange": "US" },
{ "ticker": "NVDA", "exchange": "US" },
{ "ticker": "JPM", "exchange": "US" }
],
"methods": ["MVO", "MinVol", "HRP"],
"benchmark": "sp500",
"risk_free": { "region": "US", "tenor": "3M" }
}Request Example - Rolling Walk-Forward Backtest
{
"stocks": [
{ "ticker": "RELIANCE", "exchange": "NSE" },
{ "ticker": "TCS", "exchange": "NSE" },
{ "ticker": "HDFCBANK", "exchange": "NSE" },
{ "ticker": "INFY", "exchange": "NSE" },
{ "ticker": "ICICIBANK", "exchange": "NSE" }
],
"methods": ["MVO", "HRP", "BlackLitterman"],
"benchmark": "nifty",
"rolling_backtest": {
"enabled": true,
"rebalance_frequency": "annual",
"window_type": "expanding",
"window_length_years": null
}
}Response
{
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "queued"
}curl
curl -X POST https://api.foliolab.ai/jobs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -d '{ "stocks": [ { "ticker": "RELIANCE", "exchange": "NSE" }, { "ticker": "TCS", "exchange": "NSE" }, { "ticker": "HDFCBANK", "exchange": "NSE" }, { "ticker": "INFY", "exchange": "NSE" } ], "methods": ["MVO", "HRP", "BlackLitterman"], "benchmark": "nifty" }'
Python
import requests BASE_URL = "https://api.foliolab.ai" TOKEN = "<YOUR_TOKEN>" headers = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", } # Define the portfolio and optimization methods payload = { "stocks": [ {"ticker": "RELIANCE", "exchange": "NSE"}, {"ticker": "TCS", "exchange": "NSE"}, {"ticker": "HDFCBANK", "exchange": "NSE"}, {"ticker": "INFY", "exchange": "NSE"}, ], "methods": ["MVO", "MinVol", "HRP"], "benchmark": "nifty", } response = requests.post(f"{BASE_URL}/jobs", json=payload, headers=headers) response.raise_for_status() data = response.json() print(f"Job submitted run_id={data['run_id']} status={data['status']}")
Error Codes
| Code | Name | Description |
|---|---|---|
| 40001 | INSUFFICIENT_STOCKS | Minimum 2 stocks required (when submitting stocks) |
| 40002 | INVALID_ASSET_GROUP | Either stocks or funds is required; the two cannot be mixed, nor can US and Indian stocks, and the benchmark / risk-free region must match the market |
| 40003 | INVALID_TICKER | One or more tickers not found in NSE/BSE (or US) listings |
| 40004 | INVALID_FUND_SCHEME | Scheme code unknown to MFAPI or not in the equity-only AMFI category allow-list |
| 40005 | INVALID_OPTIMIZATION_METHOD | Unknown optimization method name |
Supported Methods
All optimization strategies available through the API. Pass one or more enum values in the methods array.
MVOMean-Variance Optimization (Markowitz)MinVolMinimum Volatility portfolioMaxQuadraticUtilityMaximum quadratic utility with risk aversionEquiWeightedEqual weight (1/N) baselineHRPHierarchical Risk Parity (Lopez de Prado)MinCVaRMinimum Conditional Value at RiskMinCDaRMinimum Conditional Drawdown at RiskHERCHierarchical Equal Risk ContributionNCONested Clustered OptimizationHERC2Enhanced HERC with CVaR risk measureCriticalLineAlgorithmMarkowitz Critical Line AlgorithmBenchmarkTrackerMinimize tracking error to a benchmark indexMaximumDiversificationMaximize the diversification ratioRiskBudgetingTarget equal or custom risk contribution per assetDistributionallyRobustCVaRWorst-case CVaR over a Wasserstein ball (robust to distribution uncertainty)StackingOptimizationEnsemble meta-optimizer: stacks InverseVolatility, MaximumDiversification, and RiskBudgeting sub-modelsInverseVolatilityWeights inversely proportional to each asset's volatilitySparseMarkowitzL1L1-regularized Markowitz for sparse, concentrated portfoliosHMMRegimeMVOHidden Markov Model regime-switching MVO (regime-weighted μ and Σ)BlackLittermanBlack-Litterman with market-implied equilibrium returns and momentum viewsSupported Benchmarks
Pass one of these 14 values as the benchmark field. See Supported Benchmarks for inception dates and selection guidance.
| Value | Index | Exchange | Approx. start | History |
|---|---|---|---|---|
| nifty | NIFTY 50 | NSE | ~1990 | Full |
| sensex | BSE SENSEX 30 | BSE | ~1979 | Full |
| bank_nifty | NIFTY Bank | NSE | ~2000 | Full |
| nifty_100 | NIFTY 100 | NSE | ~2003 | Limited |
| nifty_200 | NIFTY 200 | NSE | ~2006 | Limited |
| nifty_500 | NIFTY 500 | NSE | ~1995 | Limited |
| nifty_midcap | NIFTY Midcap 150 (alias) | NSE | ~2005 | Limited |
| nifty_midcap_50 | NIFTY Midcap 50 | NSE | ~2004 | Limited |
| nifty_midcap_100 | NIFTY Midcap 100 | NSE | ~2003 | Limited |
| nifty_midcap_150 | NIFTY Midcap 150 | NSE | ~2005 | Limited |
| nifty_smallcap | NIFTY Smallcap 250 (alias) | NSE | ~2005 | Limited |
| nifty_smallcap_50 | NIFTY Smallcap 50 | NSE | ~2007 | Limited |
| nifty_smallcap_100 | NIFTY Smallcap 100 | NSE | ~2003 | Limited |
| nifty_smallcap_250 | NIFTY Smallcap 250 | NSE | ~2005 | Limited |
| sp500 | S&P 500 (SPY, total return) | US | ~1993 | Full |
| nasdaq_100 | Nasdaq-100 (QQQ, total return) | US | ~1999 | Full |
| russell_3000 | Russell 3000 (IWV, total return) | US | ~2000 | Full |
Limited-history behaviour
When you pick any benchmark flagged Limited, the optimizer truncates the asset price series to align with the benchmark window so post-optimization metrics (alpha, beta, tracking error, information ratio, etc.) are computed over a consistent overlap. This applies to both equity and mutual fund runs. Optimization itself still uses the full overlapping asset history; only the relative-to-benchmark stage is bounded by the benchmark window.
Mutual Funds
Submit optimization across Indian open-ended equity mutual funds by sending afunds array instead of stocks. Daily NAV history is sourced from MFAPI; category classification is sourced from AMFI.
Mutual fund optimization is in beta
The MF endpoint is new and still being hardened. Coverage is limited to equity-oriented schemes (large/mid/small/multi/flexi cap, ELSS, sectoral, thematic, equity index funds). Debt, hybrid, liquid, arbitrage, gilt, gold, and solution-oriented schemes are rejected. Treat outputs as exploratory and verify before using for live decisions. Full documentation: Mutual Funds (Beta).
Funds object schema
| Field | Type | Required | Description |
|---|---|---|---|
| scheme_code | integer (> 0) | Required | Numeric scheme code as published by AMFI / MFAPI (e.g. 120503). Must resolve to an equity-oriented open-ended scheme. |
| label | string | Optional | Human-readable scheme name. Used in result tables, charts, and PDF reports. Falls back to the canonical MFAPI scheme name if omitted. |
Constraints
- Minimum 2, maximum 30 schemes per run.
- Duplicate
scheme_codevalues are rejected. stocksandfundscannot both be present in the same request.benchmark = "sensex"is not available for fund runs (MF schemes are NSE-aligned in MFAPI's reference).- MFAPI NAV history typically starts in the mid-2000s, so the limited-history alignment behaviour described above always applies to MF runs.
- NAVs are growth-option, end-of-day, net of expense ratio. There is no intra-day series.
Request Example - Mutual Funds
{
"funds": [
{ "scheme_code": 120503, "label": "Parag Parikh Flexi Cap Fund - Direct Growth" },
{ "scheme_code": 118989, "label": "Mirae Asset Large Cap Fund - Direct Growth" },
{ "scheme_code": 120586, "label": "Axis Midcap Fund - Direct Growth" },
{ "scheme_code": 119598, "label": "Nippon India Small Cap Fund - Direct Growth" }
],
"methods": ["MVO", "HRP", "RiskBudgeting"],
"benchmark": "nifty_500"
}curl
curl -X POST https://api.foliolab.ai/jobs \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -d '{ "funds": [ { "scheme_code": 120503, "label": "Parag Parikh Flexi Cap" }, { "scheme_code": 118989, "label": "Mirae Asset Large Cap" }, { "scheme_code": 120586, "label": "Axis Midcap" }, { "scheme_code": 119598, "label": "Nippon Small Cap" } ], "methods": ["MVO", "HRP", "RiskBudgeting"], "benchmark": "nifty_500" }'
Python
import requests BASE_URL = "https://api.foliolab.ai" TOKEN = "<YOUR_TOKEN>" headers = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", } # Submit a mutual fund optimization (BETA) # Mutual fund runs use scheme_code (integer from MFAPI / AMFI) # instead of {ticker, exchange}. Stocks and funds cannot be mixed. payload = { "funds": [ {"scheme_code": 120503, "label": "Parag Parikh Flexi Cap"}, {"scheme_code": 118989, "label": "Mirae Asset Large Cap"}, {"scheme_code": 120586, "label": "Axis Midcap"}, {"scheme_code": 119598, "label": "Nippon Small Cap"}, ], "methods": ["MVO", "HRP", "RiskBudgeting"], "benchmark": "nifty_500", } response = requests.post(f"{BASE_URL}/jobs", json=payload, headers=headers) response.raise_for_status() data = response.json() print(f"MF job submitted run_id={data['run_id']} status={data['status']}")
Rolling Walk-Forward Backtest
Set rolling_backtest.enabled = true to run a time-series cross-validation over the full historical data instead of a single in-sample fit. The job trains on an expanding or rolling window and evaluates each period out-of-sample, producing statistically rigorous performance metrics (PSR, MinTRL, 95% CI) that account for non-Normality and serial correlation.
| Field | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | false | Must be true to activate rolling backtesting. |
| rebalance_frequency | string | "annual" | How often the portfolio is rebalanced between walk-forward periods. One of "annual", "semi_annual", "quarterly". |
| window_type | string | "expanding" | Training window strategy. "expanding" grows from the start of history (all available data up to the rebalance date); "rolling" uses a fixed-length lookback window specified by window_length_years. |
| window_length_years | number | null | null | Length of the rolling training window in years (e.g. 3 for 3-year lookback). Required when window_type = "rolling"; ignored for "expanding". |
Python - Rolling Backtest Workflow
import requests import time BASE_URL = "https://api.foliolab.ai" TOKEN = "<YOUR_TOKEN>" headers = { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", } # Submit a rolling walk-forward backtest job payload = { "stocks": [ {"ticker": "RELIANCE", "exchange": "NSE"}, {"ticker": "TCS", "exchange": "NSE"}, {"ticker": "HDFCBANK", "exchange": "NSE"}, {"ticker": "INFY", "exchange": "NSE"}, {"ticker": "ICICIBANK", "exchange": "NSE"}, ], "methods": ["MVO", "HRP", "BlackLitterman"], "benchmark": "nifty", "rolling_backtest": { "enabled": True, "rebalance_frequency": "annual", # "annual" | "semi_annual" | "quarterly" "window_type": "expanding", # "expanding" | "rolling" "window_length_years": None, # Required if window_type == "rolling" }, } job = requests.post(f"{BASE_URL}/jobs", json=payload, headers=headers) job.raise_for_status() run_id = job.json()["run_id"] print(f"Submitted rolling backtest: {run_id}") # Poll for completion while True: resp = requests.get(f"{BASE_URL}/jobs/{run_id}", headers=headers) resp.raise_for_status() status = resp.json()["status"] print(f" Status: {status}") if status in ("succeeded", "failed"): break time.sleep(3) print("Rolling backtest complete!")
Example: Full Optimization Workflow
A complete Python script that authenticates, submits an optimization job, polls for completion, and fetches the downloadable artifacts.
import requests import time BASE_URL = "https://api.foliolab.ai" # ---- Step 1: Authenticate ---- auth = requests.post(f"{BASE_URL}/auth/signin", json={ "email": "user@example.com", "password": "your-password", }) auth.raise_for_status() token = auth.json()["access_token"] headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } # ---- Step 2: Submit optimization job ---- job = requests.post(f"{BASE_URL}/jobs", json={ "stocks": [ {"ticker": "RELIANCE", "exchange": "NSE"}, {"ticker": "TCS", "exchange": "NSE"}, {"ticker": "HDFCBANK", "exchange": "NSE"}, {"ticker": "INFY", "exchange": "NSE"}, {"ticker": "ICICIBANK", "exchange": "NSE"}, ], "methods": ["MVO", "MinVol", "HRP", "HERC"], "benchmark": "nifty", }, headers=headers) job.raise_for_status() run_id = job.json()["run_id"] print(f"Submitted job: {run_id}") # ---- Step 3: Poll until completion ---- while True: status_resp = requests.get(f"{BASE_URL}/jobs/{run_id}", headers=headers) status_resp.raise_for_status() status = status_resp.json() print(f" Status: {status['status']}") if status["status"] in ("succeeded", "failed"): break time.sleep(3) # poll every 3 seconds if status["status"] == "failed": print(f"Job failed: {status.get('error_message', 'Unknown error')}") exit(1) # ---- Step 4: Fetch artifacts ---- artifacts = requests.get(f"{BASE_URL}/jobs/{run_id}/artifacts", headers=headers) artifacts.raise_for_status() for artifact in artifacts.json()["artifacts"]: print(f" {artifact['label']} -> {artifact['signed_url'][:60]}...") print("\nOptimization complete!")