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.

POST
/jobs

Submit a new portfolio optimization job for asynchronous processing.

Authentication

Required — JWT Bearer token or API Key in the Authorization header.

Request Body

FieldTypeRequiredDescription
stocksArray<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.
methodsArray<string>Default: ["MVO"]One or more optimization method enums. See Supported Methods below for the full list of 21 strategies.
benchmarkstringDefault: "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_methodstringOptionalSub-method for Critical Line Algorithm. One of "MVO", "MinVol", "Both". Only used when "CriticalLineAlgorithm" is in methods.
rolling_backtestobjectOptionalConfiguration 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_freeobjectDefault: IndiaRisk-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

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", "BlackLitterman"],
  "benchmark": "nifty"
}

Request Example - US Stocks

json
{
  "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

json
{
  "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

202 Accepted
json
{
  "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "queued"
}

curl

bash
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

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

CodeNameDescription
40001INSUFFICIENT_STOCKSMinimum 2 stocks required (when submitting stocks)
40002INVALID_ASSET_GROUPEither 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
40003INVALID_TICKEROne or more tickers not found in NSE/BSE (or US) listings
40004INVALID_FUND_SCHEMEScheme code unknown to MFAPI or not in the equity-only AMFI category allow-list
40005INVALID_OPTIMIZATION_METHODUnknown 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 portfolio
MaxQuadraticUtilityMaximum quadratic utility with risk aversion
EquiWeightedEqual weight (1/N) baseline
HRPHierarchical Risk Parity (Lopez de Prado)
MinCVaRMinimum Conditional Value at Risk
MinCDaRMinimum Conditional Drawdown at Risk
HERCHierarchical Equal Risk Contribution
NCONested Clustered Optimization
HERC2Enhanced HERC with CVaR risk measure
CriticalLineAlgorithmMarkowitz Critical Line Algorithm
BenchmarkTrackerMinimize tracking error to a benchmark index
MaximumDiversificationMaximize the diversification ratio
RiskBudgetingTarget equal or custom risk contribution per asset
DistributionallyRobustCVaRWorst-case CVaR over a Wasserstein ball (robust to distribution uncertainty)
StackingOptimizationEnsemble meta-optimizer: stacks InverseVolatility, MaximumDiversification, and RiskBudgeting sub-models
InverseVolatilityWeights inversely proportional to each asset's volatility
SparseMarkowitzL1L1-regularized Markowitz for sparse, concentrated portfolios
HMMRegimeMVOHidden Markov Model regime-switching MVO (regime-weighted μ and Σ)
BlackLittermanBlack-Litterman with market-implied equilibrium returns and momentum views

Supported Benchmarks

Pass one of these 14 values as the benchmark field. See Supported Benchmarks for inception dates and selection guidance.

ValueIndexExchangeApprox. startHistory
niftyNIFTY 50NSE~1990Full
sensexBSE SENSEX 30BSE~1979Full
bank_niftyNIFTY BankNSE~2000Full
nifty_100NIFTY 100NSE~2003Limited
nifty_200NIFTY 200NSE~2006Limited
nifty_500NIFTY 500NSE~1995Limited
nifty_midcapNIFTY Midcap 150 (alias)NSE~2005Limited
nifty_midcap_50NIFTY Midcap 50NSE~2004Limited
nifty_midcap_100NIFTY Midcap 100NSE~2003Limited
nifty_midcap_150NIFTY Midcap 150NSE~2005Limited
nifty_smallcapNIFTY Smallcap 250 (alias)NSE~2005Limited
nifty_smallcap_50NIFTY Smallcap 50NSE~2007Limited
nifty_smallcap_100NIFTY Smallcap 100NSE~2003Limited
nifty_smallcap_250NIFTY Smallcap 250NSE~2005Limited
sp500S&P 500 (SPY, total return)US~1993Full
nasdaq_100Nasdaq-100 (QQQ, total return)US~1999Full
russell_3000Russell 3000 (IWV, total return)US~2000Full

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

Beta

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

FieldTypeRequiredDescription
scheme_codeinteger (> 0)
Required
Numeric scheme code as published by AMFI / MFAPI (e.g. 120503). Must resolve to an equity-oriented open-ended scheme.
labelstringOptionalHuman-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_code values are rejected.
  • stocks and funds cannot 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

json
{
  "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

bash
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

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.

FieldTypeDefaultDescription
enabledbooleanfalseMust be true to activate rolling backtesting.
rebalance_frequencystring"annual"How often the portfolio is rebalanced between walk-forward periods. One of "annual", "semi_annual", "quarterly".
window_typestring"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_yearsnumber | nullnullLength 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

python
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.

python
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!")