Optimization
Submit portfolio optimization jobs for Indian equities, US equities, or Indian open-ended equity mutual funds across 31 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 a chart data bundle and Excel reports, and render PDF reports on request.
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, starting from uploaded holdings, and the complete 31-method catalog require Pro or Enterprise. Free accounts can submit up to 10 Indian equities per run using MVO, MinVol, CriticalLineAlgorithm, HRP, or EquiWeighted. Pro accounts can submit up to 100 equities per run. Constraints and fund-manager mode require Enterprise. The global request ceiling is 150 equities or 100 mutual funds, and 31 methods.
All optimization requests are processed asynchronously. The API immediately returns a 200 OK response with a unique run identifier. The run consumes one unit of the monthly quota at submission, whatever the number of methods. 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, maximum 150 per request (Free 10, Pro 100). 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, maximum 100. A scheme that AMFI classifies as anything other than equity is rejected. A scheme code that AMFI's current scheme master does not hold is refused with 422 UNKNOWN_FUND_SCHEME. 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 31 strategies. Duplicate method values are collapsed while preserving their first-seen order. A method that needs more assets than the request names is refused with 422 METHOD_REQUIREMENTS_UNMET; GET /methods/requirements lists each method's minimum asset count, minimum history and benchmark need. |
| benchmark | string | Default: "nifty" (retail), "nifty_500" (fund manager) | 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. A fund-manager request that names no benchmark measures against "nifty_500"; a retail request measures against "nifty". |
| benchmark_basis | string | Optional | Which series of the benchmark to measure against. "price_return" is the index level and is the default for every Indian benchmark. "total_return" uses NSE Indices' Total Return Index, which reinvests dividends, so Jensen's alpha and the information ratio are not overstated by the dividend yield. Omit it to keep the benchmark's own basis. A fund-manager request that names no basis gets "total_return" wherever the benchmark has a Total Return Index, so a fund-manager request with neither field measures against NIFTY 500 TRI. The default is written into the stored request. "total_return" is rejected for "sensex", and "price_return" is rejected for the US benchmarks, which are total-return ETFs. A backtest or Monte Carlo run on this run uses the same basis. See Price return or Total Return Index. |
| 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 also runs a walk-forward evaluation over the history, beside the 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. |
| mode | string | Default: "retail" | "retail" or "fund_manager" (Enterprise). A fund-manager request must carry constraints, every requested method must solve every constraint family it uses, and some portfolio must satisfy the constraints on the requested instruments. A request that fails either check is refused with 422 FUND_MANAGER_NOT_ADMITTED before it is queued, and spends no monthly run. The mode adds no benchmark-relative measurement beyond what a retail run reports. See Fund Manager Mode. |
| constraints | object | Optional | Hard limits on the finished weights (Enterprise). An object that sets no limit is treated as absent. See Portfolio Constraints below. |
| current_weights | object | Optional | The holdings you start from, as a map of instrument to weight (for example "RELIANCE.NS": 0.25). Stored with the run and returned by GET /jobs/{run_id}/config so a client can show the change from current to optimized weights. It does not change the optimization. Negative and non-numeric entries are dropped; values are not renormalized. Requires Pro or Enterprise. POST /holdings/import builds this map from a Zerodha or Groww holdings export. |
| ignore_short_history | boolean | Default: false | When true, an instrument with too little price history is dropped and the run continues on the rest, instead of failing. Dropped instruments are listed in the result's dropped_tickers. With constraints, a drop that leaves a rule unenforceable fails the run. |
| start_date, end_date | string (YYYY-MM-DD) | Optional | The history window of the run. Both dates are inclusive, and each one is optional. With no dates, the run uses the longest window that the assets share. The fields apply to stock, US and fund runs. A backtest or Monte Carlo run on this run uses the window of its parent. A window that ends after today, that starts on or after its end, or that holds fewer than 150 weekdays is refused with 422 HISTORY_WINDOW_REFUSED. The result records the requested window in history_window.requested. Call POST /price-history/availability first to see the stored history of each asset. |
| monte_carlo | object | Optional | A chained Monte Carlo simulation, queued on the optimized weights after the run succeeds. The body is the Monte Carlo config, and its method is required and must be one of the requested methods. Indian equities only, Pro or Enterprise, and a signed-in session (not an API key). Both the optimization and the Monte Carlo quota are consumed at submission. Results are conditional on the selected model, the frozen optimizer-time inputs, the effective configuration, the engine version and the seed; they are not calibrated forecasts. |
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,
"shadow_account": { "enabled": true, "allocation_source": "first_fold" },
"policy_comparison": { "enabled": true, "transaction_cost_bps": 10 },
"evaluation": { "cutoff_date": "2020-01-01" }
}
}Request Example - Fund Manager Mode
{
"stocks": [
{ "ticker": "TCS", "exchange": "NSE" },
{ "ticker": "INFY", "exchange": "NSE" },
{ "ticker": "HDFCBANK", "exchange": "NSE" },
{ "ticker": "ICICIBANK", "exchange": "NSE" },
{ "ticker": "ITC", "exchange": "NSE" }
],
"methods": ["MVO", "MinVol"],
"mode": "fund_manager",
"constraints": {
"every_asset": { "max_weight": 0.3 }
}
}This request names no benchmark, so it is stored and measured against "nifty_500" with "benchmark_basis": "total_return".
Response
{
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "queued"
}A stock run can also return warnings. The field is present only when there is a warning. A METHOD_HISTORY_SHORT item names a requested method that the stored prices are too short for. The run is queued, and the other methods run. The warning is a prediction from stored prices. It is not a refusal, and the absence of a warning is not a promise that the method will run.
{
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "queued",
"warnings": [
{
"code": "METHOD_HISTORY_SHORT",
"method": "HMMRegimeMVO",
"required_observations": 252,
"predicted_observations": 180,
"unit": "return observations",
"message": "HMMRegimeMVO needs at least 252 return observations, and the stored prices ..."
}
]
}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: <reasons> | Schema or market validation failed, including mixed asset groups, an invalid enum, a mismatched benchmark / risk-free region, a benchmark_basis the benchmark does not have, or a fund-manager request with no constraints. Up to five reasons follow the colon, each as field: message |
| 400 | Invalid Monte Carlo config: <reasons> | The chained monte_carlo block failed validation. The reasons follow the colon |
| 401 | Unauthorized | The bearer token or API key is missing, invalid, or expired |
| 403 | email_not_verified | The signed-in account has not confirmed its email address |
| 403 | API key lacks required scope | The API key does not carry jobs:write |
| 403 | funds_not_allowed | Mutual-fund optimization requires Pro or Enterprise |
| 403 | us_stocks_not_allowed | US-stock optimization requires Pro or Enterprise |
| 403 | holdings_upload_not_allowed | A request that carries current_weights 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 (Free 10, Pro 100) |
| 403 | constraints_not_allowed:<plan> | The request carries constraints and the plan does not include them |
| 403 | fund_manager_not_allowed:<plan> | The request sets mode to fund_manager and the plan does not include it |
| 403 | mc_api_key_not_supported | A chained monte_carlo block was sent with an API key. Chained simulations need a signed-in session |
| 404 | <saved classification message> | A saved_classifications entry names a map that does not exist on the account |
| 422 | METHOD_REQUIREMENTS_UNMET: ... | A requested method needs more assets than the request names, for example HERC with two stocks or HERC2 with three. The detail names the method, the bound and the count |
| 422 | UNKNOWN_TICKER: ... | An Indian stock symbol is not listed on the exchange that the request names. The detail names each symbol and, when it is listed elsewhere, that exchange. No monthly run is spent |
| 422 | UNKNOWN_FUND_SCHEME: ... | AMFI's current scheme master does not hold a fund scheme_code, for example a closed or merged scheme. The check does not refuse when the master cannot be read. No monthly run is spent |
| 422 | INSUFFICIENT_HISTORY: ... Insufficient: TICKER(n), ... | A stock has fewer than 150 stored daily prices in the window and ignore_short_history is false, or too few stocks would remain with it set. Refused before the run is queued, so no monthly run is spent |
| 422 | HISTORY_WINDOW_REFUSED: <reason> | start_date or end_date is after today, start_date is not before end_date, or the window holds fewer than 150 weekdays |
| 422 | REOPTIMIZE_FREQUENCY_REFUSED: <reason> | rolling_backtest.reoptimize_frequency is finer than rolling_backtest.rebalance_frequency |
| 422 | FUND_MANAGER_NOT_ADMITTED: ... | A fund-manager request failed admission: a requested method cannot solve every constraint family, or no portfolio satisfies the constraints. The detail names the method or the conflict. No monthly run is spent |
| 422 | MC_METHOD_REQUIRED | A chained monte_carlo block does not name its method |
| 422 | METHOD_NOT_REQUESTED: ... | The chained monte_carlo method is not among the requested methods |
| 429 | monthly_limit_exceeded | The account has used its monthly optimization quota |
| 503 | MC_GOVERNANCE_UNAVAILABLE | A chained monte_carlo block was sent and the service could not load its model-risk governance record. Retry later with the same request |
| 503 | Unable to enqueue run. Please retry. | The validated job could not be queued for asynchronous processing |
Supported Methods
All 31 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 measure. Needs at least 4 assets: with 3 it always returns equal weights, so use EquiWeighted for 1/NBenchmarkTrackerMinimize 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 allocationKalmanPairsTradingLong/short pairs allocation on a Kalman-filtered time-varying hedge ratio. Needs at least 2 assets and 120 observations, is absent from the web method picker, and cannot seed a Monte Carlo runSupported 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 | ~1997 | Full |
| bank_nifty | NIFTY Bank | NSE | ~2000 | Full |
| nifty_100 | NIFTY 100 | NSE | ~2003 | Limited |
| nifty_200 | NIFTY 200 | NSE | ~2004 | 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 | ~2005 | Limited |
| nifty_smallcap_100 | NIFTY Smallcap 100 | NSE | ~2004 | 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 asset price series is cut to the dates the benchmark covers. The run keeps only the dates on which every asset and the benchmark have a price, so the optimization and every metric (alpha, beta, tracking error, information ratio, etc.) use the same overlap. This applies to both equity and mutual fund runs. A Total Return Index basis can start later than the price index: NIFTY 50 TRI starts in June 1999.
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 mutual fund scheme 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, 2 to 100 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 scheme 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, 1 to 100 characters | 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 comes from AMFI's published NAVs, with MFAPI as a fallback; 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, at most 512 characters. Used in result tables, charts, and PDF reports. Falls back to the canonical 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']}")
Portfolio Constraints
Send a constraints object to put hard limits on the finished weights. Every limit binds: a limit that cannot be met makes the request infeasible and is never widened to fit. Constraints require an Enterprise plan, and a request that carries them on any other plan is refused with 403 constraints_not_allowed:<plan> before the run is queued and before it spends a monthly run.
The object takes nine keys. Send at least one limit from the first four.
| Key | What it does |
|---|---|
every_asset | One min_weight and max_weight applied to every stock in the run. |
assets | A min_weight and max_weight on one named stock at a time, given as asset. A bare symbol such as TCS binds that stock on its exchange. |
excluded_assets | Stocks held at exactly zero. |
groups | A min_weight and max_weight on the total held in one group of a classification scheme, such as a sector or mktcap band. Takes two further keys: each_member bounds each stock inside the band rather than their total, and denominator measures the percentages against another band instead of the whole portfolio. |
classifications | The sector list the group limits read, sent inline with a scheme, a vintage, a source and the assignments. |
saved_classifications | A stored sector list referenced by map_id. The run expands it at submission and records the revision it read. |
derived_classifications | Asks the server to build the mktcap bands from a market capitalisation snapshot of this run's own stocks. |
basis | What the limits are checked against. One value, final_weights, which is also the default. |
report_constraint_cost | Also measure how far each constrained portfolio sits from the one that method builds with no limits. Costs a second solve per method, so it is false by default. |
Every bound is a fraction between 0 and 1, and each bound needs at least one of min_weight and max_weight. A min_weight forces ownership: it is not conditional on the stock being held. Each list takes at most 200 rules, and at most 8 classification schemes. Not every method accepts every kind of limit. In a retail run, a method that cannot apply one produces no portfolio in that run; in a fund-manager run, the request is refused. Portfolio Constraints gives the per-method table and the mathematics behind it.
{
"stocks": [
{ "ticker": "TCS", "exchange": "NSE" },
{ "ticker": "INFY", "exchange": "NSE" },
{ "ticker": "HDFCBANK", "exchange": "NSE" },
{ "ticker": "ITC", "exchange": "NSE" }
],
"methods": ["MVO", "MinVol"],
"benchmark": "nifty",
"constraints": {
"every_asset": { "max_weight": 0.25 },
"assets": [
{ "asset": "TCS", "min_weight": 0.08, "max_weight": 0.2 }
],
"excluded_assets": ["ITC"],
"groups": [
{ "scheme": "sector", "group": "IT", "max_weight": 0.35 }
],
"classifications": [
{
"scheme": "sector",
"vintage": "2026-06-30",
"source": "desk list",
"assignments": {
"TCS": ["IT"],
"INFY": ["IT"],
"HDFCBANK": ["Financial Services"],
"ITC": ["FMCG"]
}
}
],
"report_constraint_cost": true
}
}Use POST /constraints/preflight to check the limits against a list of stocks before a run is spent. It reports whether the limits can be stated at all, whether any portfolio satisfies them together, and what each sector or mktcap band can reach under the other limits. GET /constraints/capability returns the per-method table. Both routes accept a web session only: an API key receives 403.
Rolling Walk-Forward Backtest
Set rolling_backtest.enabled = true to add a walk-forward evaluation to the run, beside the single in-sample fit. The job re-optimizes each method on an expanding or rolling training window at every rebalance date and holds the weights out of sample until the next one. The out-of-sample track carries the same metric suite as the run, with Sharpe inference (PSR, MinTRL, 95% CI) that accounts for non-Normality and serial correlation. The result is one realized historical path, not a forecast. GET /jobs/{run_id}/rolling_backtest returns this section alone. To backtest a run that already exists, use the Backtests API instead of submitting the portfolio again.
| 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". |
| reoptimize_frequency | string | null | null | How often the optimizer fits a new target. Same values as rebalance_frequency, and it must be equal to it or coarser. A finer value is refused with 422 REOPTIMIZE_FREQUENCY_REFUSED. A rebalance between two refits trades back to the last fitted target. With null, the optimizer refits at every rebalance date, as before. The field makes no claim that one policy performs better. |
| 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". |
| shadow_account | object | enabled: true | A never-rebalanced control arm held beside the walk-forward track. allocation_source is "first_fold" (default, the method's first allocation), "equal_weight", or "supplied_allocation", which requires weights. Set enabled: false to omit it. |
| policy_comparison | object | enabled: true | Compares buy and hold, rebalance to the original target, and walk-forward re-optimization on one matched evidence set. transaction_cost_bps (0 to 500, default 0) is a flat cost per unit of notional traded. |
| evaluation | object | {} | Optional cutoff_date (the first evaluation date) and end_date (the last). Both resolve to trading sessions, and the resolved dates are stored with the result. end_date must be after cutoff_date. |
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", "canceled"): 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/login", 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", "canceled"): 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]}...") # PDF reports are rendered on request: POST /jobs/{run_id}/report/pdf print("\nOptimization complete!")
Not investment advice. Past performance is not indicative of future results.