Skip to content

Baskets API

Server-side portfolio construction over the live Kalshi prediction-market universe: correlation-aware basket building, fractional Kelly sizing, NAV candles, backtests, and pre-trade validation.

These endpoints compose the primitives from the Kalshi Search API (search, similarity, clusters, correlations, model edge) with portfolio math.

Like the Chat Completions and Responses endpoints, these are direct REST endpoints called relative to the Octagon API base URL:

text
https://api.octagonai.co/v1

Conventions

Path style, field casing, timestamps, price units, and pagination follow the Kalshi Search API conventions.

Endpoints

POST /predictions/kalshi/baskets/candles

OHLC bars for a weighted basket NAV.

Request body

FieldTypeRequiredDescription
market_tickersarray of stringsYes1–100 tickers to combine into the basket.
weightsarray of numbersNoPer-ticker weights. Defaults to equal weight. Must match market_tickers length and sum to a positive value (renormalized server-side).
timeframestringNoOne of 1w, 1m, 3m, 6m, 1y. Default 1y. Drives both the lookback window and the candle bin size (see below).
TimeframeLookbackBin
1w7 days3h
1m30 days12h
3m90 days1d
6m180 days3d
1y365 days7d

Response fields

FieldTypeDescription
timeframestringThe requested timeframe.
interval_sourcestringThe candle interval the bars were built from (1h or 1d).
candlesarrayOHLC bars for the basket NAV: time (Unix-seconds bin boundary), open, high, low, close.
tickersarrayTickers included in the basket NAV.
missingarrayMarkets without data in the window — excluded from the basket NAV.

POST /predictions/kalshi/baskets/size

Apply fractional Kelly to a set of legs the caller has already picked.

Kelly formula (binary outcome)

For a leg with entry price p (the bid for the chosen side) and model probability q:

edge_pp   = (q - p) * 100
payoff b  = (1 - p) / p
fraction  = (b * q - (1 - q)) / b    # classic Kelly
f_capped  = min(max(fraction, 0), kelly_multiplier)

Sums of f_capped are scaled down so total allocation stays within the multiplier cap. Legs with no edge (q < p) get kelly_fraction = 0.

Request body

FieldTypeRequiredDescription
bankroll_usdnumberYesMust be > 0.
kelly_multipliernumberNo0 < x ≤ 1. Default 0.25 ("quarter Kelly"). Cap on total bankroll fraction.
legsarray of objectsYes1–50 entries, each { "market_ticker": string, "side": "yes" | "no", "model_probability": number }. side defaults to "yes". model_probability is a 0–1 fraction (e.g. 0.62) — values from POST /predictions/kalshi/markets/edge and GET /predictions/kalshi/markets-with-edge are on a 0–100 scale, so divide by 100 first.

Response fields

The envelope echoes bankroll_usd and kelly_multiplier, and adds:

FieldTypeDescription
total_notionalnumberTotal USD allocated across all legs.
legsarrayPer-leg sizing (see below).

Each leg in legs includes:

FieldTypeDescription
market_tickerstringThe leg's market.
sidestring"yes" or "no".
model_probabilitynumberThe supplied model probability.
pricenumberEntry price used (live bid for the chosen side).
edge_ppnumberEdge in percentage points.
kelly_fractionnumberCapped Kelly fraction for this leg.
weightnumberLeg weight within the basket.
notional_usdnumberUSD allocation for this leg.

POST /predictions/kalshi/baskets/build

One-shot diversified basket builder. Selects legs from a candidate universe subject to a per-cluster cap and a pairwise correlation cap, then sizes them by the chosen strategy.

Request body

FieldTypeRequiredDescription
universeobjectNoFilters that define the candidate pool. See below.
nintegerNoNumber of legs requested. Default 5; min 1; max 20.
max_per_clusterintegerNoCaps how many legs may share a single thematic cluster. Default 2; min 1; max 20.
max_pairwise_correlationnumberNoRange -1.0 to 1.0. Default 0.6. Rejects any candidate whose worst pairwise correlation with already-accepted legs exceeds this value.
candidate_pool_sizeintegerNoHow many markets to consider before greedy selection. Default 50; min 2; max 200.
correlation_window_daysintegerNoLookback for the correlation matrix used during selection. Default 60; min 7; max 365. Interval auto-picks 1d for ≥90 days, else 1h.
sizingobjectNoDefaults to { "strategy": "equal" }. Either { "strategy": "equal" } or { "strategy": "kelly", "bankroll_usd": ..., "kelly_multiplier": ..., "leg_probabilities": { "<market_ticker>": <probability> } }. Probabilities are 0–1 fractions, as in POST /predictions/kalshi/baskets/size. Missing probabilities default to 0.5.

universe fields

FieldTypeDescription
market_tickersarray of stringsExplicit candidate pool — 1–200 tickers. Duplicates dropped, caller order preserved, capped to candidate_pool_size.
qstringFree-text query — when set, the pool comes from semantic similarity via the same engine as GET /predictions/kalshi/markets/similar.
anchor_tickerstringSemantic anchor. Supply an existing ticker in place of free-text q.
categorystringRestrict to a category.
series_tickerstringRestrict to a series.
min_volume_24hnumberFloor on volume_24h.
close_beforedatetimeOnly markets closing before this RFC 3339 timestamp.
label_contains_anyarrayRestrict to markets whose thematic cluster label matches any of these substrings (e.g. ["fed", "cpi"]).

Universe resolution order

  1. market_tickers supplied → fetch those tickers directly; skip the search step. The structured filters (category, series_ticker, min_volume_24h, close_before) still apply as post-filters.
  2. q or anchor_ticker supplied → semantic similarity.
  3. Neither → structured-only search.

Unknown tickers and post-filter misses drop out of the candidate pool, and the request still succeeds.

Response fields

FieldTypeDescription
legsarraySelected legs (see below).
realized_max_pairwise_correlationnumberThe largest pairwise correlation among the selected legs.
cluster_breakdownobjectMap of thematic cluster ID → number of legs from that cluster.
droppedarrayRejected candidates with a reason (e.g. "cluster_cap_reached", "correlation_above_threshold").
universe_sizeintegerHow many candidates survived universe resolution and post-filters.

Each leg in legs includes: market_ticker, title, category, cluster_id, cluster_label, volume_24h, price, side, model_probability, kelly_fraction, weight, and notional_usd.

POST /predictions/kalshi/baskets/backtest

Superset of POST /predictions/kalshi/baskets/candles. Returns the same OHLC bars plus a summary block with backtest statistics computed over the basket NAV series. Request body is identical to /predictions/kalshi/baskets/candles.

Annualization details

Kalshi markets trade 24/7, so annualization uses calendar seconds rather than 252 trading days. The basket-candles bin size determines periods_per_year:

TimeframeBinperiods_per_year
1w3h~2922
1m12h~731
3m1d~365
6m3d~122
1y7d~52

Response fields

Same as /predictions/kalshi/baskets/candles, plus a summary object:

FieldTypeDescription
total_returnnumberTotal NAV return over the window.
annualized_returnnumber(1 + total_return) ** (periods_per_year / n_returns) - 1.
sharpenumber | null(mean_period_return / std_period_return) * sqrt(periods_per_year) when std > 0, else null.
max_drawdownnumberReported as a negative fraction (e.g. -0.092 = 9.2% drawdown from peak).
win_ratenumberShare of bin-to-bin returns that were positive.
first_navnumberNAV at the start of the window.
final_navnumberNAV at the end of the window.
observation_countintegerNumber of NAV observations.

POST /predictions/kalshi/baskets/validate

One-call portfolio diagnostics on a hand-built basket. Returns concentration metrics, side-aware pairwise correlations, calendar clashes, duplicate underliers, and a list of human-readable warnings.

Request body

FieldTypeRequiredDescription
legsarray of objectsYes1–50 legs, each { "market_ticker": string, "side": "yes" | "no", "stake_usd": number }. side defaults to "yes".
bankroll_usdnumberNoWhen set, max_leg_pct is computed against bankroll_usd instead of total stake.
correlation_window_daysintegerNoLookback for the pairwise correlation matrix. Default 30; min 7; max 730.
correlation_intervalstringNo"1h" or "1d". Auto-picks 1d when correlation_window_days ≥ 90, else 1h.
max_pairwise_correlationnumberNoEmits a warning when any pair exceeds this value.
calendar_clash_window_daysintegerNoWindow for grouping near-simultaneous close_time clusters. Default 7; min 1; max 90.

Side-aware: corr(YES_A, NO_B) = -corr(YES_A, YES_B), so YES/NO legs are flipped server-side.

Response fields

FieldTypeDescription
total_stake_usdnumberSum of leg stakes.
bankroll_usdnumber | nullEchoes the request value.
max_leg_pctnumberThe largest leg's share of the denominator (bankroll_usd when supplied, otherwise the total stake).
cluster_breakdown_thematicobjectMap of thematic cluster ID → leg tickers in that cluster under the current run.
cluster_breakdown_behavioralobjectMap of behavioral cluster ID → leg tickers in that cluster under the current run.
unassigned_market_tickersarrayLegs without a current-run cluster assignment.
max_pairwise_correlationnumberThe largest absolute-value pair in the correlation matrix.
pairwise_correlationsarraySide-aware pairwise correlations: ticker_a, ticker_b, correlation.
calendar_clashesarrayGroups of legs whose close_time falls inside a sliding window of size calendar_clash_window_days — surfaces "all my legs resolve on the same Fed meeting" risk. Each group: window_start, window_end, market_tickers.
duplicate_underliersarrayEvents where the basket holds more than one child market (e.g. two SpaceX-IPO strikes): event_ticker, market_tickers.
warningsarrayHuman-readable strings describing issues found in the basket.

GET /predictions/kalshi/clusters/ranked-by-return

"Show me thematic baskets that historically returned ≥ X%" in one call. For each current cluster, builds an equal-weight basket from the top-N markets by volume_24h and backtests it. Returns clusters whose total_return ≥ min_return, sorted by return descending.

Query parameters

ParameterTypeRequiredDescription
timeframestringNoOne of 1w, 1m, 3m, 6m, 1y (default 1y).
min_returnnumberNoMinimum total_return to include. Default 0.0.
top_n_per_clusterintegerNoBasket size per cluster. Default 5; min 1; max 20.
kindstringNothematic (default) or behavioral.
max_clustersintegerNoCap on how many clusters to evaluate (cost control). Default 50; min 1; max 200.

Response fields

The envelope echoes timeframe, kind, top_n_per_cluster, and min_return; each cluster in the data array includes:

FieldTypeDescription
cluster_idintegerCluster identifier under the current run.
labelstringCluster label.
descriptionstringCluster description.
sizeintegerNumber of markets in the cluster.
basket_tickersarrayThe top-N markets used to build the cluster's basket.
summaryobjectBacktest statistics for the basket (same fields as POST /predictions/kalshi/baskets/backtest).

Use Cases

  • "Find me 5 uncorrelated bets on macro themes"POST /predictions/kalshi/baskets/build with universe.label_contains_any: ["fed", "cpi", "fomc", "gdp", "jobs"], max_per_cluster: 1, and a tight max_pairwise_correlation.
  • "Build a $1000 basket on crypto"POST /predictions/kalshi/baskets/build with a category universe and Kelly sizing. leg_probabilities come from your edge engine; for Octagon-model probabilities, pull them from GET /predictions/kalshi/markets-with-edge first and feed them in.
  • Diversify a curated ticker listPOST /predictions/kalshi/baskets/build with universe.market_tickers when you already have a candidate set and want the server to handle correlation cap, per-cluster cap, and sizing over your list.
  • Pull model priors, then Kelly-sizePOST /predictions/kalshi/markets/edge for model probabilities, fed into POST /predictions/kalshi/baskets/size.
  • "Did this basket return 20%+?"POST /predictions/kalshi/baskets/backtest; read summary.total_return directly.
  • "Thematic baskets that historically returned 20%+"GET /predictions/kalshi/clusters/ranked-by-return.
  • Sanity-check a hand-built basket before placing ordersPOST /predictions/kalshi/baskets/validate; inspect warnings, max_leg_pct, calendar_clashes, and duplicate_underliers.

Errors

StatusCause
400Bad parameters — unknown name, invalid kind / timeframe, weights/tickers length mismatch, and similar.
401Missing or invalid Authorization header.
422Request body validation failure — a missing or wrongly-typed field, or a value outside its documented range.
502Upstream failure — typically a candle-pipeline query error. The detail carries only the exception class name; full diagnostics are in server logs.
503Kalshi data is unavailable — the nightly sync has not completed yet.

All errors return a JSON body with a detail field describing the cause.