Optimization

Submit portfolio optimization jobs for Indian equities using a variety of classical and modern allocation strategies.

Overview

The optimization endpoint accepts a list of Indian equity tickers (NSE/BSE) and 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, and Excel reports.

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
Array of stock objects. Each object has ticker (string, NSE/BSE symbol, e.g. "RELIANCE") and exchange (string, "NSE" or "BSE"). Minimum 2 stocks.
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 comparison. One of "nifty", "sensex", "bank_nifty".
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.

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 — 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.portfolioopt.in/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.portfolioopt.in"
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
40003INVALID_TICKEROne or more tickers not found in NSE/BSE listings
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 values as the benchmark field.

ValueIndexExchange
niftyNifty 50 IndexNSE
sensexBSE Sensex 30 IndexBSE
bank_niftyNifty Bank IndexNSE

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.portfolioopt.in"
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.portfolioopt.in"

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