Optimization
Submit portfolio optimization jobs for Indian equities, US equities, or Indian open-ended equity mutual funds across 30 classical and modern allocation strategies.
Overview
The optimization endpoint accepts Indian equity tickers (NSE/BSE), US equity tickers (NYSE/NASDAQ and other supported US venues), or Indian mutual fund scheme codes. Send equities through stocks and mutual funds through funds. The API queues the job and returns a run_id for polling. Completed jobs expose charts, Parquet data, Excel reports, and PDF reports.
Each request must contain one asset group and one market. Stocks and funds cannot be mixed, and US stocks cannot be mixed with Indian stocks. See the Mutual Funds (Beta) section for the MF schema and the Asset Discovery section for both search endpoints.
Indian requests default to the 10-year India government bond seriesINDIRLTLT01STM from FRED. US requests must use a US benchmark and set risk_free.region to "US", with an optional US tenor. If live yield data is unavailable, the backend uses its configured fallback rate.
Plan access: US equities, mutual funds, and the complete 30-method catalog require Pro or Enterprise. Free accounts can submit up to 10 Indian equities per run using MVO, MinVol, CriticalLineAlgorithm, HRP, or EquiWeighted. The global request ceiling is 100 assets and 30 methods.
All optimization requests are processed asynchronously. The API immediately returns a 200 OK 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 30 strategies. Duplicate method values are collapsed while preserving their first-seen order. |
| 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 government bond series via FRED) or "US". For "US", optionally choose a tenor - "FedFunds" (overnight effective fed funds rate) or a Treasury constant-maturity tenor ("1M", "3M", "6M", "1Y" … "30Y") - sourced from FRED. Omitting the tenor defaults to "10Y". 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 | Response detail | Description |
|---|---|---|
| 400 | Invalid optimization request payload | Schema or market validation failed, including mixed asset groups, an invalid enum, or a mismatched benchmark / risk-free region |
| 401 | Unauthorized | The bearer token or API key is missing, invalid, or expired |
| 403 | funds_not_allowed | Mutual-fund optimization requires Pro or Enterprise |
| 403 | us_stocks_not_allowed | US-stock optimization requires Pro or Enterprise |
| 403 | algo_not_allowed:<method> | The selected plan does not include one of the requested methods |
| 403 | ticker_limit_exceeded:<limit> | The request exceeds the plan's per-run equity limit |
| 429 | monthly_limit_exceeded | The account has used its monthly optimization quota |
| 503 | Unable to enqueue run. Please retry. | The validated job could not be queued for asynchronous processing |
Supported Methods
All 30 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) baselineCriticalLineAlgorithmMarkowitz Critical Line AlgorithmHRPHierarchical Risk Parity (Lopez de Prado)MinCVaRMinimum Conditional Value at RiskMinCDaRMinimum Conditional Drawdown at RiskHERCHierarchical Equal Risk ContributionNCONested Clustered OptimizationHERC2Enhanced HERC with CVaR risk measureBenchmarkTrackerMinimize 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 viewsEWMAMVOEWMA moments that adapt MVO to recent volatility regimesMinSemivarianceMinimum downside semivarianceMinEVaRMinimum Entropic Value-at-RiskMinEDaRMinimum Entropic Drawdown-at-RiskMaxDecorrelationMaximum decorrelation through the asset correlation matrixRobustMVOWorst-case MVO under expected-return uncertaintyResampledMVOBootstrap-resampled MVO with averaged weightsSparseIndexTrackingSparse benchmark replication with a reduced asset subsetQuintileMomentumEqual-weight allocation to the highest-momentum quintileKellyLong-only growth-optimal expected log-wealth allocationSupported Benchmarks
Pass one of these 17 values (14 Indian and 3 US) 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.
Asset Discovery
Public search endpoints for resolving mutual-fund scheme codes and US equity tickers before building a POST /jobs payload. Authentication is not required for either search endpoint.
/mutual-funds/searchSearch the MFAPI master list. Use asset_class=equity when selecting schemes for optimization because non-equity schemes can be discovered but are rejected by POST /jobs.
| Query parameter | Rules | Description |
|---|---|---|
| q | Required, at least 2 characters | Scheme, AMC, or category search text |
| limit | Default 20, range 1-50 | Flat-response result cap when pagination is not requested |
| asset_class | equity, debt, hybrid, or other | Optional AMFI asset-class filter |
| page / page_size | Page starts at 1; size defaults to 20 and caps at 50 | Supplying either parameter enables pagination and adds page, page_size, total, and has_more to the response |
curl "https://api.foliolab.ai/mutual-funds/search?q=parag%20parikh&asset_class=equity&page=1&page_size=20"{
"items": [
{
"scheme_code": 122639,
"scheme_name": "Parag Parikh Flexi Cap Fund - Direct Plan - Growth",
"isin_growth": "INF879O01027",
"isin_div_reinvestment": null,
"category": "Flexi Cap Fund",
"asset_class": "equity"
}
],
"page": 1,
"page_size": 20,
"total": 1,
"has_more": false
}Returns HTTP 400 for invalid query parameters and HTTP 503 when the MFAPI catalog is temporarily unavailable.
/us-stocks/searchSearch Yahoo Finance for US-listed equities. Results exclude ETFs, indices, futures, foreign-exchange pairs, and non-US listings.
| Query parameter | Rules | Description |
|---|---|---|
| q | Required, non-empty | Company name or ticker search text |
| limit | Default 20, range 1-50 | Maximum number of filtered US equities to return |
curl "https://api.foliolab.ai/us-stocks/search?q=apple&limit=10"{
"items": [
{
"ticker": "AAPL",
"name": "Apple Inc.",
"exchange": "NASDAQ",
"type": "EQUITY"
}
]
}exchange is a display venue such as NASDAQ or NYSE. In a job request, always send { "ticker": "AAPL", "exchange": "US" }.Returns HTTP 400 for invalid query parameters and HTTP 503 when Yahoo Finance search is temporarily unavailable.
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 unique schemes, maximum 100 submitted schemes per run.
- Duplicate
scheme_codevalues are deduplicated before optimization. stocksandfundscannot both be present in the same request.- Use an Indian benchmark and the Indian risk-free region. US benchmarks are rejected for fund runs.
- Available NAV history varies by scheme and may shorten the common analysis window.
- 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!")