Results
Retrieve optimization results, performance metrics, and analytics
/jobs/{run_id}/resultRetrieve the full optimization result for a job. A job that has not succeeded returns 200 with its status, timing, error_code, error_message and error_detail, and methods: null. You must own the run.
Authentication
Required. Bearer token (JWT) or API key with jobs:read. You must be the owner of the run, and an API key reads only the runs its own client submitted.
Path Parameters
run_idstringThe unique identifier returned when the optimization job was createdResponse Structure
Top-level Fields
run_idstringUnique identifier for this optimization runstatusstringThe job status. Only a "succeeded" job carries methods and responsetimingobjectContains startedAt, finishedAt (ISO 8601), and durationSecondsrolling_backtest_configobject?The rolling_backtest block of the request, or nullmethodsarray?One entry per method that produced a portfolio (see below). null until the job succeedsdropped_tickersarrayRequested instruments that did not contribute to the weights, each with a reason such as no_data, stale_data or insufficient_historyhistory_windowobject?Present only when the run recorded it. start, end and rows of the analysis window. limited_by names the late tickers that cut the start by more than one year, each with its first_date, and start_without gives the start without them. requested holds the start_date and end_date of the request, present only when the request named a window. The same object is also in responseresponseobjectPer-method results and run-level data (see below)Methods Array
Each element in the methods array represents one optimization method's results:
methodstringOptimization method identifier, e.g. "MVO", "HRP"weightsobjectInstrument to weight mapping, e.g. {"INFY.NS": 0.35, "RELIANCE.NS": 0.65}. NSE tickers end in .NS, BSE in .BO, US tickers are bare, and funds are keyed by scheme codemetricsobjectAll performance metrics (see Performance Metrics Reference below)portfolio_yearly_returnsobjectCalendar year to return mapping for this portfolio methodyearly_returnsobjectThe same mapping as portfolio_yearly_returnsrolling_betasobjectCalendar year to beta against the benchmark, e.g. {"2021": 0.92}. yearly_betas carries the same mappingcompute_started_atstringISO 8601 timestamp when this method began computingcompute_finished_atstringISO 8601 timestamp when this method finishedcompute_duration_secondsnumberWall-clock time to compute this method in secondsmethod_labelstringDisplay label. Differs from method when the method is degraded, e.g. "MVO (RF-clipped)"base_methodstringOriginal method name before degradationis_degradedbooleanTrue if the risk-free rate was clipped to fit the feasible regiondegradationobject|string?Structured metadata or string describing the RF-clipping (type, reason, rates, epsilon)resultobjectThe complete stored result for the methodResponse Object
The top-level response object contains aggregated data and signed URLs:
resultsobjectPer-method data keyed by method: weights, performance (the metrics), rolling_betas, portfolio_yearly_returns, risk_attribution, compute timing, and degradation metadatachart_bundlestring|objectPre-computed chart data: cumulative returns per method, benchmark returns, rolling correlation, returns distribution, covariance, spectral decomposition, risk attribution, stationarity tests, method errors and the rolling backtest. The first read soon after the job finishes may carry the object inline; every other read carries a signed URL, valid for one hour, to the gzip-compressed JSONcumulative_returnsobjectEmpty. Read the cumulative series from chart_bundledatesarrayEmpty. Read the dates from chart_bundlebenchmark_returnsarrayThe benchmark name, with an empty returns list. Read the series from chart_bundlestock_yearly_returnsobjectPer-stock yearly returns keyed by tickerrisk_free_ratenumberThe annual risk-free rate used for performance metric computationsstart_datestringStart date of the analysis windowend_datestringEnd date of the analysis windowrolling_backtest_configobject?The request's rolling_backtest block, or nulldropped_tickersarrayThe same list as the top-level dropped_tickersmetaobjectContains run_id and timingSharpe Inference Object
Each per-method entry under results[method].performance.sharpe_inference carries statistical inference for the Sharpe ratio. Probabilistic Sharpe Ratio (PSR) and Minimum Track Record Length (MinTRL) are always present; the Sharpe Stability Ratio (SSR) fields populate only when enough rolling Sharpe observations exist; the Deflated Sharpe Ratio (DSR) fields populate only when the trial count N is above 1 and the trial Sharpes vary. N counts every successful candidate with a finite Sharpe across the account's search campaign for the same market and asset type, not only the methods of this run. The campaign is frozen when the run is admitted, and a run without that context falls back to its own method list; dsr_basis says which applied:
sr_estimatenumberSharpe estimate at observation frequency (not annualized)sr_std_errornumberGeneralized sampling standard error of the Sharpe estimatesr_annualizednumberAnnualized Sharpe ratio used for displayskewnessnumberSample skewness of returns, used in the PSR standard errorkurtosisnumberPearson kurtosis of returns (3 for a Normal distribution)autocorrelationnumberAR(1) autocorrelation of returnstotal_observationsnumberNumber of return observations T used in the inferencepsrnumberProbabilistic Sharpe Ratio: probability the Sharpe exceeds psr_thresholdpsr_thresholdnumberBenchmark Sharpe SR_0 used as the PSR null hypothesis (usually 0)min_trlnumberMinimum Track Record Length in observations for significancemin_trl_yearsnumberMinTRL expressed in approximate years (min_trl / 252)ci_lowernumberLower bound of the 95% confidence interval for the annualized Sharpeci_uppernumberUpper bound of the 95% confidence interval for the annualized Sharpeperiod_sharpesarrayPer-period Sharpe values (populated for rolling backtests; empty otherwise)ssrnumber?Sharpe Stability Ratio: mean rolling Sharpe over HAC long-run std (null if unavailable)ssr_t_statnumber?SSR HAC t-statistic, asymptotically N(0,1) under the nullssr_p_valuenumber?One-sided SSR p-value versus the benchmark Sharpessr_mean_rolling_srnumber?Mean of the rolling Sharpe seriesssr_hac_stdnumber?HAC (Newey-West) long-run standard deviation of the rolling Sharpe seriesssr_windownumber?Rolling window length in observationsssr_bandwidthnumber?Newey-West bandwidth (Andrews 1991 plug-in)ssr_rolling_observationsnumberCount of rolling Sharpe observations (0 if SSR unavailable)dsrnumber?Deflated Sharpe Ratio in [0,1]: probability the Sharpe is real after correcting for selection bias across N trials. Null unless N > 1dsr_sr_starnumber?Deflated benchmark SR*_0: expected maximum annualized Sharpe under zero skill across N trials. Null unless N > 1dsr_n_trialsnumber?N: the number of trials the deflation corrects fordsr_trial_variancenumber?Annualized variance of the N trial Sharpes. Null unless N > 1dsr_basisstring?Whether N came from the frozen search campaign or from this run's method list. dsr_basis_note explains it in wordsdsr_raw_trial_count / dsr_effective_trial_countnumber?The raw and effective trial counts. They are equal: no dependence discount is applieddsr_campaign_run_countnumber?How many runs contributed trials to the campaignExample Response
{
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "succeeded",
"timing": {
"startedAt": "2025-01-15T10:30:00.000Z",
"finishedAt": "2025-01-15T10:30:47.000Z",
"durationSeconds": 47.2
},
"methods": [
{
"method": "MVO",
"weights": {
"INFY.NS": 0.2541,
"RELIANCE.NS": 0.3102,
"TCS.NS": 0.1893,
"HDFCBANK.NS": 0.2464
},
"metrics": {
"expected_return": 0.1847,
"cagr": 0.1623,
"volatility": 0.2134,
"max_drawdown": -0.2891,
"sharpe": 0.8742,
"sortino": 1.2156,
"treynor_ratio": 0.1203,
"omega_ratio": 1.4521,
"calmar_ratio": 0.5141,
"sterling_ratio": 0.8800,
"v2_ratio": 1.0345,
"information_ratio": 0.3241,
"upside_potential_ratio": 1.8923,
"modigliani_risk_adjusted_performance": 0.1456,
"var_95": 0.0207,
"cvar_95": 0.0309,
"var_90": 0.0147,
"cvar_90": 0.0241,
"evar_95": 0.0646,
"dar_95": 0.2573,
"cdar_95": 0.3246,
"portfolio_beta": 0.8734,
"portfolio_alpha": 0.0523,
"beta_pvalue": 0.0001,
"alpha_pvalue": 0.0912,
"beta_stderr": 0.0121,
"alpha_stderr": 0.0309,
"residual_volatility": 0.0987,
"regression_observations": 1236,
"r_squared": 0.7845,
"blume_adjusted_beta": 0.9156,
"welch_beta": 0.8612,
"semi_beta": 0.9423,
"vasicek_beta": 0.8891,
"james_stein_beta": 0.8945,
"skewness": -0.3421,
"kurtosis": 4.2156,
"entropy": 3.1245,
"gini_mean_difference": 0.0187,
"ulcer_index": 0.1203,
"tracking_error": 0.0845,
"upside_capture": 1.3386,
"downside_capture": 0.7192,
"effective_n": 3.42,
"romad": 0.5141,
"sharpe_inference": { "psr": 0.9871, "min_trl_years": 2.1, "...": "..." }
},
"yearly_returns": {
"2020": 0.1789,
"2021": 0.3102,
"2022": -0.0645,
"2023": 0.2156,
"2024": 0.1456
},
"portfolio_yearly_returns": {
"2020": 0.1789,
"2021": 0.3102,
"2022": -0.0645,
"2023": 0.2156,
"2024": 0.1456
},
"rolling_betas": {
"2020": 0.9234,
"2021": 0.8745,
"2022": 0.8512
},
"compute_started_at": "2025-01-15T10:30:02.000Z",
"compute_finished_at": "2025-01-15T10:30:15.000Z",
"compute_duration_seconds": 13.4
}
// ... additional methods
],
"dropped_tickers": [],
"response": {
"results": {
"MVO": {
"method": "MVO",
"weights": { "...": "..." },
"performance": { "...": "..." },
"risk_attribution": { "...": "..." },
"compute": { "started_at": "...", "finished_at": "...", "duration_seconds": 13.4 }
}
},
"chart_bundle": "<presigned-url - expires in 1h>",
"cumulative_returns": {},
"dates": [],
"benchmark_returns": [{ "name": "nifty", "returns": [] }],
"stock_yearly_returns": {
"INFY.NS": { "2020": 0.85, "2021": 0.32, "2022": -0.12, ... },
"RELIANCE.NS": { "2020": 0.30, "2021": 0.18, "2022": 0.05, ... }
},
"risk_free_rate": 0.065,
"start_date": "2020-01-01",
"end_date": "2024-12-31",
"meta": {
"run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timing": {
"startedAt": "2025-01-15T10:30:00.000Z",
"finishedAt": "2025-01-15T10:30:47.000Z",
"durationSeconds": 47.2
}
}
}
}curl
curl -X GET https://api.foliolab.ai/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890/result \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
Python
import requests response = requests.get( "https://api.foliolab.ai/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890/result", headers={"Authorization": "Bearer YOUR_JWT_TOKEN"} ) result = response.json() # Access MVO weights mvo = next(m for m in result["methods"] if m["method"] == "MVO") print("MVO Weights:", mvo["weights"]) print("Sharpe Ratio:", mvo["metrics"]["sharpe"]) print("Max Drawdown:", mvo["metrics"]["max_drawdown"]) # Chart series live in the chart bundle: inline on the first read, # otherwise a signed URL to gzip-compressed JSON bundle = result["response"]["chart_bundle"] if isinstance(bundle, str): bundle = requests.get(bundle).json() cum_returns = [p for p in bundle["cumulative_returns"] if p["method"] == "MVO"]
Performance Metrics Reference
Comprehensive reference of all metrics returned in the metrics object for each optimization method. Metrics are grouped by category. Every figure is realized on the run's own analysis window, which is in sample. The object also carries risk_decomposition and sharpe_inference.
Sign conventions for the loss measures
var_95, cvar_95, var_90, cvar_90, evar_95, dar_95 and cdar_95 are all positive loss magnitudes. A larger number is a larger loss. This follows Rockafellar and Uryasev, who define these measures on a loss variable rather than on a return.
A negative value is meaningful rather than an error: it says the tail quantile is still a gain.
Runs created before this change stored var_95, cvar_95, var_90 and cvar_90 as negative return quantiles. If you hold your own copy of an older response, read the sign before you compare it against a new one.
Returns
expected_returnExpected ReturnRealized annualized arithmetic mean of the portfolio's daily returns over the analysis window (daily mean x 252). Equal to realized_expected_return0.05 - 0.30cagrCAGRCompound Annual Growth Rate over the analysis window0.02 - 0.25model_expected_returnModel Expected ReturnThe method's own estimate from its expected-return input, where the method produces one. model_volatility and model_sharpe sit beside it. These are estimates, not measurementsvariesRisk
volatilityVolatilityRealized annualized standard deviation of daily portfolio returns. Equal to realized_volatility0.10 - 0.40max_drawdownMaximum DrawdownLargest peak-to-trough decline during the analysis window-0.60 - 0.00var_95VaR (95%)One-day Value-at-Risk at 95%, as a positive loss magnitude0.01 - 0.04cvar_95CVaR (95%)One-day Conditional VaR (Expected Shortfall) at 95%, as a positive loss magnitude0.02 - 0.06var_90VaR (90%)One-day Value-at-Risk at 90%, as a positive loss magnitude0.01 - 0.03cvar_90CVaR (90%)One-day Conditional VaR at 90%, as a positive loss magnitude0.015 - 0.045evar_95EVaR (95%)Entropic Value-at-Risk at 95%, as a positive loss magnitude0.035 - 0.10dar_95DaR (95%)Drawdown-at-Risk at 95%, the 5% quantile of the drawdown series, as a positive magnitude0.10 - 0.60cdar_95CDaR (95%)Conditional Drawdown-at-Risk at 95%, the mean of the worst 5% of drawdowns, as a positive magnitude0.12 - 0.67Ratios
sharpeSharpe RatioRealized annualized excess return over the risk-free rate, divided by volatility. Equal to realized_sharpe-1.0 - 3.0sortinoSortino RatioDownside risk-adjusted return using semi-deviation-1.0 - 5.0treynor_ratioTreynor RatioExcess return per unit of systematic (beta) risk-0.10 - 0.30omega_ratioOmega RatioProbability-weighted ratio of gains to losses0.5 - 3.0calmar_ratioCalmar RatioCAGR divided by the absolute maximum drawdown, over the full sample. Carries the same number as romad0.25 - 2.6sterling_ratioSterling RatioCAGR divided by the average annual drawdown plus 10 points0.4 - 2.4v2_ratioV2 RatioAnnualized return of the portfolio relative to the benchmark, divided by the root mean square of the relative drawdowns-2.0 - 5.0information_ratioInformation RatioAnnualized active return divided by tracking error vs. benchmark. Against a price-return benchmark the numerator is biased upward by about the index dividend yield-2.0 - 2.0upside_potential_ratioUpside Potential RatioRatio of upside potential to downside risk0.5 - 4.0modigliani_risk_adjusted_performanceM² (Modigliani)Risk-adjusted performance normalized to benchmark volatility0.0 - 0.30Beta & Regression
portfolio_betaPortfolio BetaSensitivity of portfolio returns to the benchmark0.3 - 1.5portfolio_alphaPortfolio AlphaJensen's alpha: excess return not explained by benchmark exposure (annualized). Against a price-return benchmark it is biased upward by about the index dividend yield-0.10 - 0.15beta_pvalueBeta p-valueStatistical significance of the beta estimate0.0 - 1.0alpha_pvalueAlpha p-valueTwo-sided p-value of the alpha estimate0.0 - 1.0beta_stderrBeta Std. ErrorStandard error of the beta estimate0.005 - 0.05alpha_stderrAlpha Std. ErrorStandard error of the alpha estimate, annualized like the alpha0.01 - 0.10residual_volatilityResidual VolatilityAnnualized standard deviation of the regression residual (idiosyncratic risk)0.03 - 0.25regression_observationsRegression ObservationsNumber of daily observations in the regression. Zero means the regression did not run0 - Nr_squaredR-squaredProportion of portfolio variance explained by the benchmark0.0 - 1.0blume_adjusted_betaBlume Adjusted BetaBeta adjusted toward 1.0 using Blume's mean-reversion formula0.5 - 1.3welch_betaWelch-Style BetaRobust beta using slope-bounded clipping of portfolio excess returns0.3 - 1.5semi_betaSemi BetaBeta calculated using only downside return observations0.3 - 2.0vasicek_betaVasicek BetaBayesian-shrinkage beta using Vasicek's adjustment0.4 - 1.4james_stein_betaJames-Stein BetaShrinkage estimator that reduces extreme beta estimates0.5 - 1.3coskewnessCoskewnessCo-skewness of portfolio excess returns with benchmark excess returns-1.0 - 1.0cokurtosisCokurtosisCo-kurtosis of portfolio excess returns with benchmark excess returns0.0 - 10.0Distribution
skewnessSkewnessAsymmetry of the return distribution (negative = left tail)-2.0 - 1.0kurtosisKurtosisTail heaviness of the return distribution (excess kurtosis)0.0 - 10.0entropyEntropyShannon entropy of the portfolio weights, -sum(w_i ln w_i). Higher means capital is spread more evenly; the maximum is ln N0.0 - ln NOther
gini_mean_differenceGini Mean DifferenceAverage absolute difference between all pairs of returns0.005 - 0.05ulcer_indexUlcer IndexRoot mean squared drawdown. Stored as a fraction; reports print it as a percentage0.04 - 0.31tracking_errorTracking ErrorStandard deviation of the portfolio's excess returns vs. benchmark0.02 - 0.25upside_captureUpside CaptureRatio of geometric average returns in the benchmark's up months. 1.0 matches the benchmark0.84 - 2.27downside_captureDownside CaptureSame ratio over the benchmark's down months. Below 1.0 is better; a negative value means the portfolio gained0.12 - 1.31effective_nEffective N (Inverse HHI)Effective number of holdings implied by weight concentration, 1 / sum(w_i^2)1.0 - NromadRoMaDReturn over maximum drawdown. The same expression as calmar_ratio, so the two always agree0.25 - 2.6Analytics Endpoints
Dashboard summaries, saved run comparisons, and insights for the analytics layer. Every response is wrapped as { "ok": true, "data": ... }.
/analytics/dashboardRetrieve a dashboard summary of your runs: headline statistics, per-method statistics, per-benchmark alpha, daily run counts, recent runs, standalone and inline backtest statistics, the in-sample against out-of-sample gap, saved comparisons, and generated insights.
Authentication
Required. Bearer token (JWT) or API key. Returns data scoped to the authenticated user.
Query Parameters
All optional. market (IN, US or all), mode (retail or fund_manager), constraints (with or without), benchmark, basis (price_return or total_return), tag, and the inclusive creation dates from and to narrow the run-derived blocks. An unknown value returns 400 with the valid ones. With no market, the view opens on the market of your latest succeeded run and filters.defaultedMarket is true. Pass market=all to combine markets.
Example Response
{
"ok": true,
"data": {
"summary": {
"totalRuns": 42,
"runsSucceeded": 38,
"runsFailed": 3,
"runsCanceled": 1,
"successRate": 0.905,
"runsThisMonth": 9,
"avgSharpe": 0.92,
"bestSharpe": 1.41,
"mostUsedMethod": "HRP",
"mostUsedBenchmark": "nifty",
"...": "..."
},
"methodStats": [
{ "method": "MVO", "count": 15, "avgSharpe": 0.92, "avgCagr": 0.18, "winRate": 0.4, "...": "..." }
],
"benchmarkStats": [ ... ],
"alphaByBenchmark": [ ... ],
"dailyRunCounts": [
{ "runDate": "2025-01-10", "totalRuns": 3, "succeeded": 3, "failed": 0 }
],
"recentRuns": [ ... ],
"savedComparisons": [ ... ],
"backtestSummary": { ... },
"backtestMethodStats": [ ... ],
"recentBacktests": [ ... ],
"inSampleVsOutOfSample": [ ... ],
"cohortAnalytics": { ... },
"insights": [
{
"insightId": "0d3c...",
"type": "...",
"priority": 1,
"title": "...",
"message": "...",
"relatedRunIds": ["..."]
}
],
"filters": { "applied": { ... }, "available": { ... }, "mixed": false, "defaultedMarket": true }
}
}Averages are taken over your own runs and describe their in-sample analysis windows. They are not a measure of how any method will perform.
curl
curl -X GET "https://api.foliolab.ai/analytics/dashboard?market=IN" \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
Python
import requests response = requests.get( "https://api.foliolab.ai/analytics/dashboard", params={"market": "IN"}, headers={"Authorization": "Bearer YOUR_JWT_TOKEN"} ) dashboard = response.json()["data"] print("Total Runs:", dashboard["summary"]["totalRuns"]) for insight in dashboard["insights"]: print(insight["title"])
/analytics/comparisonsCreate a saved comparison to group multiple optimization runs for side-by-side analysis. GET /analytics/comparisons lists them with limit (1 to 100, default 10) and offset.
Authentication
Required. JWT session only (not available via API key).
Request Body
{
"name": "Q4 2024 Strategy Comparison",
"description": "Comparing MVO vs HRP across different stock sets",
"runIds": [
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"f9e8d7c6-b5a4-3210-fedc-ba9876543210"
]
}name is required (at most 200 characters), description is optional (at most 2,000), and runIds lists 1 to 100 of your own run ids. A run you do not own returns 403. Runs from two markets cannot share a comparison and return 400.
Response
{
"ok": true,
"data": {
"comparisonId": "4c2a9f1e-...",
"name": "Q4 2024 Strategy Comparison",
"description": "Comparing MVO vs HRP across different stock sets",
"runCount": 2,
"methods": ["HRP", "MVO"],
"isPinned": false,
"bestSharpe": 1.12,
"createdAt": "2025-01-15T12:00:00Z",
"updatedAt": "2025-01-15T12:00:00Z"
}
}curl
curl -X POST https://api.foliolab.ai/analytics/comparisons \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Q4 2024 Strategy Comparison", "runIds": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890", "f9e8d7c6-b5a4-3210-fedc-ba9876543210"] }'
Python
import requests response = requests.post( "https://api.foliolab.ai/analytics/comparisons", headers={"Authorization": "Bearer YOUR_JWT_TOKEN"}, json={ "name": "Q4 2024 Strategy Comparison", "runIds": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890", "f9e8d7c6-b5a4-3210-fedc-ba9876543210"] } ) comparison = response.json()["data"] print("Comparison ID:", comparison["comparisonId"])
/analytics/comparisons/{comparison_id}Retrieve a saved comparison with a summary of each run and a comparability verdict that says whether the runs differ in market, mode, constraints, benchmark or basis. GET /analytics/comparisons/{comparison_id}/surface returns the full side-by-side surface (every metric group, weight overlap, out-of-sample results and curves), five runs per page by default, with page_size, cursor, curve_methods and groups.
Authentication
Required. JWT session only (not available via API key).
Response
{
"ok": true,
"data": {
"comparison": {
"comparisonId": "4c2a9f1e-...",
"name": "Q4 2024 Strategy Comparison",
"description": "Comparing MVO vs HRP across different stock sets",
"runCount": 2,
"summaryCache": { ... },
"isPinned": false,
"createdAt": "2025-01-15T12:00:00Z",
"updatedAt": "2025-01-15T12:00:00Z"
},
"runs": [
{
"runId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "succeeded",
"createdAt": "2025-01-14T09:00:00Z",
"benchmark": "nifty",
"stockCount": 5,
"runSummary": { ... },
"hasConstraints": false,
"mode": "retail"
}
],
"comparability": { "verdict": "...", "differs": [ ... ], "facetsByRun": { ... }, "reasons": [ ... ] }
}
}curl
curl -X GET https://api.foliolab.ai/analytics/comparisons/COMPARISON_ID \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
Python
import requests response = requests.get( "https://api.foliolab.ai/analytics/comparisons/COMPARISON_ID", headers={"Authorization": "Bearer YOUR_JWT_TOKEN"} ) data = response.json()["data"] for run in data["runs"]: print(f"Run {run['runId']}: {run['status']}, stocks={run['stockCount']}")
/analytics/comparisons/{comparison_id}Update a saved comparison. All fields are optional: only include the fields you want to change.
Authentication
Required. JWT session only (not available via API key).
Request Body
namestring?New name for the comparisondescriptionstring?New descriptionisPinnedboolean?Pin or unpin the comparison on the dashboardaddRunIdsstring[]?Run IDs to add to this comparison. An add that would mix markets returns 400removeRunIdsstring[]?Run IDs to remove from this comparison{
"name": "Updated Comparison Name",
"isPinned": true,
"addRunIds": ["0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e"],
"removeRunIds": ["f9e8d7c6-b5a4-3210-fedc-ba9876543210"]
}The response is the updated comparison in the same shape as the comparison object above.
curl
curl -X PATCH https://api.foliolab.ai/analytics/comparisons/COMPARISON_ID \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Comparison Name", "isPinned": true }'
Python
import requests response = requests.patch( "https://api.foliolab.ai/analytics/comparisons/COMPARISON_ID", headers={"Authorization": "Bearer YOUR_JWT_TOKEN"}, json={"name": "Updated Comparison Name", "isPinned": True} ) print("Updated:", response.json()["data"])
/analytics/comparisons/{comparison_id}Permanently delete a saved comparison. This does not delete the underlying runs.
Authentication
Required. JWT session only (not available via API key).
Response
{
"ok": true,
"deleted": true
}curl
curl -X DELETE https://api.foliolab.ai/analytics/comparisons/COMPARISON_ID \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
/analytics/refresh-statsRegenerate your insights. The dashboard statistics are computed live on each request, so there is nothing else to refresh. An insight you dismissed with POST /analytics/insights/{insight_id}/dismiss stays dismissed.
Authentication
Required. JWT session only (not available via API key).
Response
{
"ok": true,
"refreshed": true,
"updatedAt": "2025-01-15T12:30:00Z",
"insights": { "generated": 3, "expired": 1, "failed": 0 }
}curl
curl -X POST https://api.foliolab.ai/analytics/refresh-stats \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
Tags
Organize and label your optimization runs with custom tags for filtering and grouping. Filter your history by tag with GET /jobs/history?tag=....
/runs/{run_id}/tagsAdd a tag to an optimization run. Tags have a name and optional color for visual identification. Adding a tag the run already carries updates its color.
Authentication
Required. JWT session only (not available via API key).
Request Body
{
"tagName": "production",
"tagColor": "#22c55e"
}Response
{
"ok": true,
"tagged": true
}curl
curl -X POST https://api.foliolab.ai/runs/RUN_ID/tags \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"tagName": "production", "tagColor": "#22c55e"}'
/runs/{run_id}/tags/{tag_name}Remove a tag from an optimization run by tag name. A tag the run does not carry returns 404.
Authentication
Required. JWT session only (not available via API key).
Response
{
"ok": true,
"deleted": true
}curl
curl -X DELETE https://api.foliolab.ai/runs/RUN_ID/tags/production \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
/analytics/tagsList all unique tags across your optimization runs with usage counts.
Authentication
Required. JWT session only (not available via API key).
Response
{
"ok": true,
"data": [
{ "tagName": "production", "tagColor": "#22c55e", "runCount": 8 },
{ "tagName": "experimental", "tagColor": "#f59e0b", "runCount": 5 },
{ "tagName": "backtest-q4", "tagColor": "#3b82f6", "runCount": 3 }
]
}Python
import requests response = requests.get( "https://api.foliolab.ai/analytics/tags", headers={"Authorization": "Bearer YOUR_JWT_TOKEN"} ) for tag in response.json()["data"]: print(f"{tag['tagName']} ({tag['runCount']} runs)")
Not investment advice. Past performance is not indicative of future results.