Jobs
Track optimization status, browse job history, and download result artifacts.
Job Lifecycle
Every optimization job progresses through a fixed set of states.
queued
Job is accepted and waiting for a worker to pick it up.
running
Optimization is actively being computed. Market data has been fetched.
uploading
Computation complete. Results and artifacts are being uploaded to storage.
succeeded
All methods completed. Artifacts are ready for download.
failed
An error occurred. Check error_code and error_detail for diagnostics.
/jobs/{run_id}Check the current status of an optimization job.
Authentication
Required - you must own the run (the job must have been submitted under your account).
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| run_id | UUID | The unique identifier returned when the job was submitted. |
Response
Successful job:
{
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "succeeded",
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:02Z",
"finished_at": "2025-01-15T10:31:14Z",
"error_code": null,
"error_message": null,
"error_detail": null
}Failed job:
{
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "failed",
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:02Z",
"finished_at": "2025-01-15T10:30:08Z",
"error_code": 50002,
"error_message": "DATA_FETCH_ERROR",
"error_detail": "Unable to fetch price data for ticker INVALIDTICKER.NSE"
}curl
curl https://api.foliolab.ai/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer <YOUR_TOKEN>"
Python
import requests BASE_URL = "https://api.foliolab.ai" TOKEN = "<YOUR_TOKEN>" RUN_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" headers = {"Authorization": f"Bearer {TOKEN}"} response = requests.get(f"{BASE_URL}/jobs/{RUN_ID}", headers=headers) response.raise_for_status() job = response.json() print(f"Status: {job['status']}") if job["status"] == "failed": print(f"Error: {job['error_message']} - {job['error_detail']}")
/jobs/historyList your optimization job history with cursor-based pagination.
Authentication
Required - returns only jobs belonging to the authenticated user.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| limit | integer | 20 | Number of items per page. Maximum 100. |
| cursor | string | - | Opaque pagination cursor. Pass the next_cursor value from the previous response to fetch the next page. |
| q | string | - | Search by run_id prefix. Useful for quickly finding a specific job. |
Response
{
"items": [
{
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "succeeded",
"created_at": "2025-01-15T10:30:00Z",
"methods": ["MVO", "HRP"],
"stock_count": 5
},
{
"run_id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
"status": "running",
"created_at": "2025-01-15T11:00:00Z",
"methods": ["MinVol"],
"stock_count": 8
}
],
"next_cursor": "eyJjIjoiMjAyNS0wMS0xNVQxMDozMDowMFoiLCJpIjoiYTFiMmMzZDQifQ=="
}Pagination note
This endpoint uses cursor-based pagination. When next_cursor is null, you have reached the last page. Do not attempt to construct or decode cursor values - treat them as opaque strings.
curl
# First page curl "https://api.foliolab.ai/jobs/history?limit=10" \ -H "Authorization: Bearer <YOUR_TOKEN>" # Next page (use next_cursor from previous response) curl "https://api.foliolab.ai/jobs/history?limit=10&cursor=eyJjIjoiMjAyNS0wMS..." \ -H "Authorization: Bearer <YOUR_TOKEN>"
Python
import requests BASE_URL = "https://api.foliolab.ai" TOKEN = "<YOUR_TOKEN>" headers = {"Authorization": f"Bearer {TOKEN}"} # Fetch all jobs using cursor-based pagination all_jobs = [] cursor = None while True: params = {"limit": 20} if cursor: params["cursor"] = cursor resp = requests.get(f"{BASE_URL}/jobs/history", params=params, headers=headers) resp.raise_for_status() data = resp.json() all_jobs.extend(data["items"]) cursor = data.get("next_cursor") if not cursor: break print(f"Total jobs: {len(all_jobs)}") for job in all_jobs: print(f" {job['run_id'][:8]}... {job['status']} ({job['stock_count']} stocks)")
/jobs/{run_id}/artifactsRetrieve downloadable artifacts (charts, data files, reports) for a completed job.
Authentication
Required - you must own the run.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| run_id | UUID | The job identifier. Job must have status succeeded. |
Response
{
"artifacts": [
{
"artifact_id": "art_001",
"method": "MVO",
"kind": "returns_dist",
"label": "MVO Returns Distribution",
"signed_url": "<presigned-url:art_001 - expires in 1h>",
"content_type": "image/png"
},
{
"artifact_id": "art_002",
"method": "MVO",
"kind": "max_drawdown",
"label": "MVO Maximum Drawdown",
"signed_url": "<presigned-url:art_002 - expires in 1h>",
"content_type": "image/png"
},
{
"artifact_id": "art_global_001",
"method": null,
"kind": "covariance_heatmap",
"label": "Covariance Heatmap",
"signed_url": "<presigned-url:art_global_001 - expires in 1h>",
"content_type": "image/png"
},
{
"artifact_id": "art_global_002",
"method": null,
"kind": "optimization_report",
"label": "Optimization Report (Excel)",
"signed_url": "<presigned-url:art_global_002 - expires in 1h>",
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}
],
"data_parquets": {
"benchmark_returns": "<presigned-url:benchmark_returns - expires in 1h>",
"cumulative_returns": "<presigned-url:cumulative_returns - expires in 1h>",
"stock_yearly_returns": "<presigned-url:stock_yearly_returns - expires in 1h>"
},
"data_files": {
"chart_bundle": "<presigned-url:chart_bundle - expires in 1h>"
}
}Artifact Types
| Kind | Scope | Content Type | Description |
|---|---|---|---|
| data | Global | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | Excel report (label: "optimization_report") |
| data | Global | application/json | Pre-computed chart data bundle - gzip-compressed JSON (label: "chart_bundle") |
| report_pdf | Global | application/pdf | PDF report (current format). Legacy runs may use kind="data" + label="optimization_report_pdf" |
| data | Global | application/parquet | Benchmark daily returns (label: "benchmark_returns") |
| data | Global | application/parquet | Cumulative portfolio returns (label: "cumulative_returns") |
| data | Global | application/parquet | Yearly returns per stock (label: "stock_yearly_returns") |
Signed URL expiry & storage access
Artifacts are always retrieved through the Folio Lab API - there is no separate public storage hostname you can browse. Each call to /jobs/{run_id}/artifacts returns short-lived presigned URLs that expire after 1 hour and are opaque (you cannot construct or re-sign them yourself). When a URL expires, call the endpoint again to receive fresh ones; the underlying artifact is unchanged.
curl
curl https://api.foliolab.ai/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890/artifacts \ -H "Authorization: Bearer <YOUR_TOKEN>"
Python - Download All Artifacts
import requests import os BASE_URL = "https://api.foliolab.ai" TOKEN = "<YOUR_TOKEN>" RUN_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" headers = {"Authorization": f"Bearer {TOKEN}"} # Fetch artifact metadata resp = requests.get(f"{BASE_URL}/jobs/{RUN_ID}/artifacts", headers=headers) resp.raise_for_status() data = resp.json() # Download each artifact using its signed URL output_dir = f"./results/{RUN_ID[:8]}" os.makedirs(output_dir, exist_ok=True) for artifact in data["artifacts"]: ext = "png" if "png" in artifact["content_type"] else "xlsx" filename = f"{artifact['kind']}_{artifact['method'] or 'global'}.{ext}" filepath = os.path.join(output_dir, filename) print(f"Downloading {artifact['label']}...") download = requests.get(artifact["signed_url"]) download.raise_for_status() with open(filepath, "wb") as f: f.write(download.content) # Download Parquet data files for name, url in data["data_parquets"].items(): filepath = os.path.join(output_dir, f"{name}.parquet") print(f"Downloading {name}.parquet...") download = requests.get(url) with open(filepath, "wb") as f: f.write(download.content) print(f"\nAll artifacts saved to {output_dir}/")
Error Codes
Job-specific error codes returned in the error_code and error_message fields.
| Code | Name | Description |
|---|---|---|
| 40401 | NOT_FOUND | Job not found or not owned by the authenticated user |
| 50001 | OPTIMIZATION_FAILED | Optimization computation failed during processing |
| 50002 | DATA_FETCH_ERROR | Could not fetch market data for one or more tickers |
| 50099 | UNEXPECTED_ERROR | An unexpected server error occurred |
Monte Carlo Simulation Endpoints
Run a forward Monte Carlo wealth simulation on a succeeded optimization run and poll its result. These endpoints authenticate with a web session or an MCP OAuth grant; API keys currently receive 403 mc_api_key_not_supported. The complete request schema lives on the dedicated Monte Carlo API page.
| Endpoint | Purpose |
|---|---|
| POST /runs/{run_id}/monte_carlo | Submit a simulation on a succeeded run. Body is the Monte Carlo config; method is required and must be one of the parent run's methods. Returns 202 with mc_run_id. |
| GET /runs/{run_id}/monte_carlo | List all simulations for a parent run, newest first. |
| GET /mc/{mc_run_id} | Status, config echo, inline summary, and a one-hour signed artifact URL once succeeded. |
| GET /mc | Cursor-paginated list of all your simulations across runs. |
Key config fields
method (required), horizon_years (1-40, default 15), path_count (1,000-100,000, default 10,000), starting_wealth, contributions / withdrawals (SIP / SWP), goal_amount + goal_basis, rgp (return process, see below), scenarios (2008_repeat, covid_repeat, single_name_blowup, bear_regime_start, permanent_loss, liquidity_freeze, multi_session_price_band, correlation_spike), rebalance_frequency, rebalance_mode with its walk_forward block, paired_static_baseline, risk_free, precision, parameter_uncertainty, and seed for reproducibility. Unknown fields are rejected rather than silently ignored.
Return processes (rgp)
The return process decides how simulated paths are generated. None of these has yet passed out-of-sample validation against the others, so none is published as more accurate. Running two and reporting the range is more honest than trusting one; the spread across processes is model risk, not noise.
| Value | Model | Plan |
|---|---|---|
| fhs_gjr_garch_vt | Default. Politis-Romano stationary block bootstrap over the parent run's daily returns. Retains observed marginal shocks, complete same-day cross-asset rows, and local ordering conditional on the selected block length. Expected block length uses Politis-White automatic selection with a sqrt(T) fallback; override with expected_block_length_days. This is the only process that replays real calendar windows, so the 2008 and COVID stresses require it. | Pro, Enterprise |
| regime_bootstrap | Hamilton Markov regime switching fitted on portfolio returns, resampling within the active regime's labelled days. Configure with regime.n_regimes (2-3) and regime.start. A failed fit falls back to block_bootstrap with the reason reported in the result's regime block. Required by the bear_regime_start scenario. | Pro, Enterprise |
| fhs_ewma | Filtered historical simulation. Per-asset EWMA conditional volatility is estimated on drift-removed log returns, complete cross-asset rows of standardized residuals are resampled, and future volatility is rebuilt recursively. Unlike the bootstrap it conditions on the current volatility level. Tune with fhs.ewma_lambda (default 0.94, a benchmark value, not a validated one) and fhs.long_run_blend_half_life_days. Needs about 3 years of shared history. | Pro, Enterprise |
| fhs_gjr_garch | Filtered historical simulation with a per-asset GJR-GARCH(1,1) volatility model, so losses raise future volatility more than equivalent gains. Each asset's fit must pass convergence, parameter validity, a persistence guard, an unconditional-variance sanity band and a residual Ljung-Box check; any failure falls back to EWMA for that asset with the reason recorded per asset in return_generator.per_asset. Needs about 5 years of shared history. | Enterprise |
| gbm_lognormal | Daily lognormal GBM: IID Gaussian log returns with a Ledoit-Wolf shrinkage covariance. An analytical and implementation benchmark, deliberately not a realism upgrade, since it has neither volatility clustering nor fat tails. | Pro, Enterprise |
| student_t | Multivariate Student-t on simple returns, covariance-matched to history; student_t_dof defaults to 5. A comparison process: it emits unbounded simple returns and relies on the disclosed gross-return clip. | Pro, Enterprise |
| gaussian | Multivariate normal on simple returns. Understates tail risk; kept for comparison only, with the same unbounded support caveat as student_t. | Pro, Enterprise |
The three log-domain processes (fhs_ewma, fhs_gjr_garch, gbm_lognormal) build gross returns as expm1 of a log return, so returns are positive by construction and no clipping correction applies. They reconstruct returns rather than replaying historical rows, so they expose no sampled calendar window: inflation uses the disclosed constant model instead of calendar-aligned CPI replay. Runs using them publish a return_generator block carrying the fitted model, residual-pool size, ACF diagnostics, admission thresholds and any per-asset fallbacks.
Every process has a methodology page covering its mathematics, calibration and admission rules: Monte Carlo Engine Methodology.
Drift (drift.mode)
historical (default) simulates the portfolio's own historical arithmetic mean. override recentres to annual_return_override, an annualized daily arithmetic mean (divided by 252), not a CAGR target. shrunk blends the two using an explicit shrink_weight. forward_shrunk instead derives the weight on history from precision: you declare the anchor and its uncertainty via anchor_uncertainty_annual (one annualized standard deviation), and the weight on history becomes var_prior / (var_prior + var_hac), where var_hac is the Newey-West HAC sampling variance of the portfolio's daily mean. A confident anchor pulls toward the anchor; a vague one defers to history. The resolved weight is reported in calibration.drift. That forward anchor is a user-declared assumption. market_forward_shrunk is the fifth mode: it applies the same precision weighting toward a versioned, audited market-level Indian equity anchor resolved by policy, and it accepts no user-supplied anchor fields at all. Drift modes are plan-gated by mc_allowed_drift_modes, where a "*" wildcard deliberately excludes the house-view modes and a missing key degrades to the four caller-supplied modes (403 mc_drift_mode_not_allowed:<mode>). Separately, drift.uncertainty (default on) gives each path a HAC mean perturbation; that is sampling uncertainty around a fixed estimate, not forward-assumption or structural uncertainty.
Plan limits and quota errors
- Free: no access (
403 mc_not_allowed). - Pro: 20 simulations / month and up to 50,000 paths per run (
429 mc_monthly_limit_exceeded,403 mc_path_limit_exceeded). - Enterprise: unlimited simulations, up to 100,000 paths per run.
- All plans: at most 2 simulations queued or running at once (
429 mc_inflight_limit_exceeded); the parent run must be succeeded (409 parent_not_ready); the portfolio must be long-only Indian cash equity (422 with a stable eligibility code otherwise). - Return processes are plan-gated:
403 mc_process_not_allowed:<process>means thatrgpis not on your plan (fhs_gjr_garch is Enterprise only). The authoritative list for the signed-in user isfeature_flags.mc_allowed_processesonGET /billing/me, where"*"means every process. 422 mc_insufficient_history_for_processmeans the chosen process needs more jointly aligned daily history than the portfolio has (about 3 years for fhs_ewma, 5 for the GJR family). Omit rgp and the default falls back on its own, or retry with a longer shared history.503 MC_GOVERNANCE_UNAVAILABLEmeans the service could not load or apply its model-risk governance record. This is a service-side problem, not a request error: keep the configuration unchanged and try again later. It applies to direct simulation submission and toPOST /jobsrequests containing a chainedmonte_carloblock.
Not investment advice. Past performance is not indicative of future results.