Appearance
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/v1Conventions
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
| Field | Type | Required | Description |
|---|---|---|---|
market_tickers | array of strings | Yes | 1–100 tickers to combine into the basket. |
weights | array of numbers | No | Per-ticker weights. Defaults to equal weight. Must match market_tickers length and sum to a positive value (renormalized server-side). |
timeframe | string | No | One of 1w, 1m, 3m, 6m, 1y. Default 1y. Drives both the lookback window and the candle bin size (see below). |
| Timeframe | Lookback | Bin |
|---|---|---|
1w | 7 days | 3h |
1m | 30 days | 12h |
3m | 90 days | 1d |
6m | 180 days | 3d |
1y | 365 days | 7d |
Response fields
| Field | Type | Description |
|---|---|---|
timeframe | string | The requested timeframe. |
interval_source | string | The candle interval the bars were built from (1h or 1d). |
candles | array | OHLC bars for the basket NAV: time (Unix-seconds bin boundary), open, high, low, close. |
tickers | array | Tickers included in the basket NAV. |
missing | array | Markets 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
| Field | Type | Required | Description |
|---|---|---|---|
bankroll_usd | number | Yes | Must be > 0. |
kelly_multiplier | number | No | 0 < x ≤ 1. Default 0.25 ("quarter Kelly"). Cap on total bankroll fraction. |
legs | array of objects | Yes | 1–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:
| Field | Type | Description |
|---|---|---|
total_notional | number | Total USD allocated across all legs. |
legs | array | Per-leg sizing (see below). |
Each leg in legs includes:
| Field | Type | Description |
|---|---|---|
market_ticker | string | The leg's market. |
side | string | "yes" or "no". |
model_probability | number | The supplied model probability. |
price | number | Entry price used (live bid for the chosen side). |
edge_pp | number | Edge in percentage points. |
kelly_fraction | number | Capped Kelly fraction for this leg. |
weight | number | Leg weight within the basket. |
notional_usd | number | USD 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
| Field | Type | Required | Description |
|---|---|---|---|
universe | object | No | Filters that define the candidate pool. See below. |
n | integer | No | Number of legs requested. Default 5; min 1; max 20. |
max_per_cluster | integer | No | Caps how many legs may share a single thematic cluster. Default 2; min 1; max 20. |
max_pairwise_correlation | number | No | Range -1.0 to 1.0. Default 0.6. Rejects any candidate whose worst pairwise correlation with already-accepted legs exceeds this value. |
candidate_pool_size | integer | No | How many markets to consider before greedy selection. Default 50; min 2; max 200. |
correlation_window_days | integer | No | Lookback for the correlation matrix used during selection. Default 60; min 7; max 365. Interval auto-picks 1d for ≥90 days, else 1h. |
sizing | object | No | Defaults 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
| Field | Type | Description |
|---|---|---|
market_tickers | array of strings | Explicit candidate pool — 1–200 tickers. Duplicates dropped, caller order preserved, capped to candidate_pool_size. |
q | string | Free-text query — when set, the pool comes from semantic similarity via the same engine as GET /predictions/kalshi/markets/similar. |
anchor_ticker | string | Semantic anchor. Supply an existing ticker in place of free-text q. |
category | string | Restrict to a category. |
series_ticker | string | Restrict to a series. |
min_volume_24h | number | Floor on volume_24h. |
close_before | datetime | Only markets closing before this RFC 3339 timestamp. |
label_contains_any | array | Restrict to markets whose thematic cluster label matches any of these substrings (e.g. ["fed", "cpi"]). |
Universe resolution order
market_tickerssupplied → fetch those tickers directly; skip the search step. The structured filters (category,series_ticker,min_volume_24h,close_before) still apply as post-filters.qoranchor_tickersupplied → semantic similarity.- Neither → structured-only search.
Unknown tickers and post-filter misses drop out of the candidate pool, and the request still succeeds.
Response fields
| Field | Type | Description |
|---|---|---|
legs | array | Selected legs (see below). |
realized_max_pairwise_correlation | number | The largest pairwise correlation among the selected legs. |
cluster_breakdown | object | Map of thematic cluster ID → number of legs from that cluster. |
dropped | array | Rejected candidates with a reason (e.g. "cluster_cap_reached", "correlation_above_threshold"). |
universe_size | integer | How 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:
| Timeframe | Bin | periods_per_year |
|---|---|---|
1w | 3h | ~2922 |
1m | 12h | ~731 |
3m | 1d | ~365 |
6m | 3d | ~122 |
1y | 7d | ~52 |
Response fields
Same as /predictions/kalshi/baskets/candles, plus a summary object:
| Field | Type | Description |
|---|---|---|
total_return | number | Total NAV return over the window. |
annualized_return | number | (1 + total_return) ** (periods_per_year / n_returns) - 1. |
sharpe | number | null | (mean_period_return / std_period_return) * sqrt(periods_per_year) when std > 0, else null. |
max_drawdown | number | Reported as a negative fraction (e.g. -0.092 = 9.2% drawdown from peak). |
win_rate | number | Share of bin-to-bin returns that were positive. |
first_nav | number | NAV at the start of the window. |
final_nav | number | NAV at the end of the window. |
observation_count | integer | Number 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
| Field | Type | Required | Description |
|---|---|---|---|
legs | array of objects | Yes | 1–50 legs, each { "market_ticker": string, "side": "yes" | "no", "stake_usd": number }. side defaults to "yes". |
bankroll_usd | number | No | When set, max_leg_pct is computed against bankroll_usd instead of total stake. |
correlation_window_days | integer | No | Lookback for the pairwise correlation matrix. Default 30; min 7; max 730. |
correlation_interval | string | No | "1h" or "1d". Auto-picks 1d when correlation_window_days ≥ 90, else 1h. |
max_pairwise_correlation | number | No | Emits a warning when any pair exceeds this value. |
calendar_clash_window_days | integer | No | Window 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
| Field | Type | Description |
|---|---|---|
total_stake_usd | number | Sum of leg stakes. |
bankroll_usd | number | null | Echoes the request value. |
max_leg_pct | number | The largest leg's share of the denominator (bankroll_usd when supplied, otherwise the total stake). |
cluster_breakdown_thematic | object | Map of thematic cluster ID → leg tickers in that cluster under the current run. |
cluster_breakdown_behavioral | object | Map of behavioral cluster ID → leg tickers in that cluster under the current run. |
unassigned_market_tickers | array | Legs without a current-run cluster assignment. |
max_pairwise_correlation | number | The largest absolute-value pair in the correlation matrix. |
pairwise_correlations | array | Side-aware pairwise correlations: ticker_a, ticker_b, correlation. |
calendar_clashes | array | Groups 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_underliers | array | Events where the basket holds more than one child market (e.g. two SpaceX-IPO strikes): event_ticker, market_tickers. |
warnings | array | Human-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
| Parameter | Type | Required | Description |
|---|---|---|---|
timeframe | string | No | One of 1w, 1m, 3m, 6m, 1y (default 1y). |
min_return | number | No | Minimum total_return to include. Default 0.0. |
top_n_per_cluster | integer | No | Basket size per cluster. Default 5; min 1; max 20. |
kind | string | No | thematic (default) or behavioral. |
max_clusters | integer | No | Cap 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:
| Field | Type | Description |
|---|---|---|
cluster_id | integer | Cluster identifier under the current run. |
label | string | Cluster label. |
description | string | Cluster description. |
size | integer | Number of markets in the cluster. |
basket_tickers | array | The top-N markets used to build the cluster's basket. |
summary | object | Backtest 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/buildwithuniverse.label_contains_any: ["fed", "cpi", "fomc", "gdp", "jobs"],max_per_cluster: 1, and a tightmax_pairwise_correlation. - "Build a $1000 basket on crypto" —
POST /predictions/kalshi/baskets/buildwith a category universe and Kelly sizing.leg_probabilitiescome from your edge engine; for Octagon-model probabilities, pull them fromGET /predictions/kalshi/markets-with-edgefirst and feed them in. - Diversify a curated ticker list —
POST /predictions/kalshi/baskets/buildwithuniverse.market_tickerswhen 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-size —
POST /predictions/kalshi/markets/edgefor model probabilities, fed intoPOST /predictions/kalshi/baskets/size. - "Did this basket return 20%+?" —
POST /predictions/kalshi/baskets/backtest; readsummary.total_returndirectly. - "Thematic baskets that historically returned 20%+" —
GET /predictions/kalshi/clusters/ranked-by-return. - Sanity-check a hand-built basket before placing orders —
POST /predictions/kalshi/baskets/validate; inspectwarnings,max_leg_pct,calendar_clashes, andduplicate_underliers.
Errors
| Status | Cause |
|---|---|
400 | Bad parameters — unknown name, invalid kind / timeframe, weights/tickers length mismatch, and similar. |
401 | Missing or invalid Authorization header. |
422 | Request body validation failure — a missing or wrongly-typed field, or a value outside its documented range. |
502 | Upstream failure — typically a candle-pipeline query error. The detail carries only the exception class name; full diagnostics are in server logs. |
503 | Kalshi data is unavailable — the nightly sync has not completed yet. |
All errors return a JSON body with a detail field describing the cause.
Related docs
- Kalshi Search API — the primitives these endpoints build on.
- Prediction Markets Agent — the agent that produces narrative reports on individual events.
- Events API — latest snapshot of every analyzed event plus per-event history.