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.

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 30 strategies. Duplicate method values are collapsed while preserving their first-seen order.
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 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

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

200 OK
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

CodeResponse detailDescription
400Invalid optimization request payloadSchema or market validation failed, including mixed asset groups, an invalid enum, or a mismatched benchmark / risk-free region
401UnauthorizedThe bearer token or API key is missing, invalid, or expired
403funds_not_allowedMutual-fund optimization requires Pro or Enterprise
403us_stocks_not_allowedUS-stock optimization requires Pro or Enterprise
403algo_not_allowed:<method>The selected plan does not include one of the requested methods
403ticker_limit_exceeded:<limit>The request exceeds the plan's per-run equity limit
429monthly_limit_exceededThe account has used its monthly optimization quota
503Unable 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 portfolio
MaxQuadraticUtilityMaximum quadratic utility with risk aversion
EquiWeightedEqual weight (1/N) baseline
CriticalLineAlgorithmMarkowitz Critical Line Algorithm
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
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
EWMAMVOEWMA moments that adapt MVO to recent volatility regimes
MinSemivarianceMinimum downside semivariance
MinEVaRMinimum Entropic Value-at-Risk
MinEDaRMinimum Entropic Drawdown-at-Risk
MaxDecorrelationMaximum decorrelation through the asset correlation matrix
RobustMVOWorst-case MVO under expected-return uncertainty
ResampledMVOBootstrap-resampled MVO with averaged weights
SparseIndexTrackingSparse benchmark replication with a reduced asset subset
QuintileMomentumEqual-weight allocation to the highest-momentum quintile
KellyLong-only growth-optimal expected log-wealth allocation

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

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.

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.

GET
/mutual-funds/search
Beta

Search 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 parameterRulesDescription
qRequired, at least 2 charactersScheme, AMC, or category search text
limitDefault 20, range 1-50Flat-response result cap when pagination is not requested
asset_classequity, debt, hybrid, or otherOptional AMFI asset-class filter
page / page_sizePage starts at 1; size defaults to 20 and caps at 50Supplying either parameter enables pagination and adds page, page_size, total, and has_more to the response
bash
curl "https://api.foliolab.ai/mutual-funds/search?q=parag%20parikh&asset_class=equity&page=1&page_size=20"
json
{
  "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.

GET
/us-stocks/search

Search Yahoo Finance for US-listed equities. Results exclude ETFs, indices, futures, foreign-exchange pairs, and non-US listings.

Query parameterRulesDescription
qRequired, non-emptyCompany name or ticker search text
limitDefault 20, range 1-50Maximum number of filtered US equities to return
bash
curl "https://api.foliolab.ai/us-stocks/search?q=apple&limit=10"
json
{
  "items": [
    {
      "ticker": "AAPL",
      "name": "Apple Inc.",
      "exchange": "NASDAQ",
      "type": "EQUITY"
    }
  ]
}
The search response's 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

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 unique schemes, maximum 100 submitted schemes per run.
  • Duplicate scheme_code values are deduplicated before optimization.
  • stocks and funds cannot 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

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