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.
/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 | Array of stock objects. Each object has ticker (string, NSE/BSE symbol, e.g. "RELIANCE") and exchange (string, "NSE" or "BSE"). Minimum 2 stocks. |
| methods | Array<string> | Default: ["MVO"] | One or more optimization method enums. See Supported Methods below for the full list of 21 strategies. |
| benchmark | string | Default: "nifty" | Benchmark index for comparison. One of "nifty", "sensex", "bank_nifty". |
| 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. |
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 — 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.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
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
| Code | Name | Description |
|---|---|---|
| 40001 | INSUFFICIENT_STOCKS | Minimum 2 stocks required |
| 40003 | INVALID_TICKER | One or more tickers not found in NSE/BSE listings |
| 40005 | INVALID_OPTIMIZATION_METHOD | Unknown 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 portfolioMaxQuadraticUtilityMaximum quadratic utility with risk aversionEquiWeightedEqual weight (1/N) baselineHRPHierarchical Risk Parity (Lopez de Prado)MinCVaRMinimum Conditional Value at RiskMinCDaRMinimum Conditional Drawdown at RiskHERCHierarchical Equal Risk ContributionNCONested Clustered OptimizationHERC2Enhanced HERC with CVaR risk measureCriticalLineAlgorithmMarkowitz Critical Line AlgorithmBenchmarkTrackerMinimize 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 viewsSupported Benchmarks
Pass one of these values as the benchmark field.
| Value | Index | Exchange |
|---|---|---|
| nifty | Nifty 50 Index | NSE |
| sensex | BSE Sensex 30 Index | BSE |
| bank_nifty | Nifty Bank Index | NSE |
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.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.
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!")