Skip to content
Yes No Odds

11 min read ·

How to Chart Kalshi Bitcoin Markets Against Exchange Prices

Align exchange BTC/USD, live BRTI, and Kalshi bids and asks by timestamp, then plot dollar values and event probability on separate axes.

Share X in f
Adrian Foss · 11 min read

A useful Kalshi Bitcoin price versus exchange chart is a custom, timestamp-aligned visualization. Plot exchange BTC/USD, the live BRTI, and the contract-specific settlement estimate on a dollar axis. Plot the selected Kalshi event contract’s bid and ask on a separate 0%–100% probability axis. The comparison is valid only when it follows that contract’s exact benchmark, strike or band, expiration timestamp, time zone, and settlement rules.

The reviewed Kalshi and CF Benchmarks resources do not provide a ready-made synchronized overlay. The reproducible workflow below shows how to record the required series, align them by timestamp, calculate the final-window estimate, and plot them without treating a binary contract price as a Bitcoin quote.

The chart to use: three comparisons, not one

Choose the chart mode according to the question you want to answer.

Chart mode Series to compare Axis What it answers
Exchange versus benchmark Exchange BTC/USD and live BRTI Dollars How does one venue differ from the multi-venue benchmark?
Exchange versus settlement estimate Exchange BTC/USD and the contract-specific window average Dollars What benchmark value could be used for resolution under the stated rules?
Exchange versus event contract Exchange BTC/USD plus Kalshi bid and ask Dollars and probability How do market odds change as Bitcoin moves relative to the outcome definition?

Exchange BTC/USD, BRTI, and the benchmark average are dollar-valued measurements, so they can share one vertical axis. A Kalshi event-contract price cannot. It prices a defined binary claim rather than Bitcoin itself and belongs on a second axis running from 0% to 100%.

Use the best Yes bid and ask as a shaded probability band. This exposes the spread and prevents a stale last trade from appearing more current than the order book. A midpoint or last-trade line may be added, but the legend must identify the field precisely.

Every chart should label:

  • Kalshi market ticker
  • Contract type: directional, threshold, or range
  • Strike or range boundaries
  • Expiration timestamp and time zone
  • Exchange venue and trading pair
  • Sampling interval
  • Kalshi field: bid, ask, midpoint, or last trade
  • Benchmark and official observation window

For a reproducible example, use KXBTCD-26SEP1022 as the illustrative ticker and “Coinbase BTC-USD midpoint” as the exchange comparator. That ticker is a historical example, not a recommendation or a substitute for an active market. Before collecting data, replace the configuration with the current ticker and copy its actual rules, strike, benchmark, and timing fields.

The minimum input files are:

exchange.csv
timestamp,bid,ask

brti.csv
timestamp,brti

kalshi.csv
timestamp,yes_bid,yes_ask,last_price

Use ISO 8601 UTC timestamps, such as 2026-09-10T21:59:59Z. Normalize Kalshi prices to decimal dollars between 0 and 1 before plotting them as percentages. Keep source timestamps and local receipt timestamps in the underlying archive even if the simplified plotting files contain only one timestamp.

Why Kalshi can disagree with an exchange screen

Kalshi says its cryptocurrency event contracts settle against the applicable CF Benchmarks Real-Time Index rather than the displayed price from one exchange. The relevant market and its rules identify the index, measurement time, and settlement details.

For Bitcoin, the applicable benchmark is commonly the CME CF Bitcoin Real Time Index, or BRTI. CF Benchmarks describes the BRTI as an index that aggregates order data from qualifying Bitcoin-USD markets operated by multiple major exchanges. It is therefore a different measurement from the latest trade or midpoint on one venue.

Four values must remain separate:

  1. Exchange BTC/USD: A price constructed from one venue’s order book or trades.
  2. Live BRTI: A multi-venue benchmark value.
  3. Provisional window-to-date mean: The mean of eligible benchmark observations collected so far within the official settlement window.
  4. Official expiration value: The final value used to resolve the contract under its rules.

An individual exchange has its own order book, liquidity, latency, and sequence of trades, while BRTI aggregates qualifying markets. A difference between the two displays does not by itself show that either is wrong or that manipulation occurred.

Timing can widen the difference. An exchange screen might show its latest trade at the displayed expiration time, while the contract can resolve from multiple benchmark observations across a defined window. A brief price movement may affect one exchange’s last print differently from the benchmark average.

How the final-minute settlement calculation works

For the general cryptocurrency-contract process documented by Kalshi, the expiration value is calculated from 60 applicable index observations collected once per second during the final minute before expiration. Kalshi’s crypto-market documentation describes the benchmark, 60-observation calculation, and RTI data pass-through.

The basic calculation is:

Final window mean = \sumi₌₁⁶⁰ I_i ÷ 60

where I_i is an eligible index observation within the contract’s specified window.

A defensible calculation follows five steps:

  1. Collect timestamped observations from t-59 through t, or use the exact start and end timestamps stated in the rules.
  2. Confirm that all required observations belong to the official window.
  3. Apply any contract-specific eligibility rules or filters.
  4. Calculate the arithmetic mean of the eligible observations as directed.
  5. Retain the observations, timestamps, rule version, and calculation output for an audit trail.

The chart may contain three different average lines, but they need distinct labels:

  • Provisional window-to-date mean: An expanding mean that begins with the first eligible observation in the official window and uses no earlier values.
  • Final window mean: The mean calculated after all required eligible observations are available.
  • Trailing 60-second diagnostic: A rolling mean that may include observations from before the official window.

Do not label the trailing diagnostic as a settlement estimate if it contains out-of-window observations. Likewise, the provisional mean is an indicator, not the official expiration value.

Mark the beginning and end of the official window with vertical lines or a shaded region. Publish the final mean only after verifying that all required observations are present. The official settlement value should remain separately labeled because an independent reconstruction may omit filters or other rule-specific processing.

Do not assume every Bitcoin contract uses identical timing or filters. Confirm the named index, expiration timestamp, time zone, observation window, exclusions, and averaging rule on the current market page. Benchmark publication frequency and Kalshi’s contract-specific sampling method are separate concepts.

A Reddit user described a BRTI chart with a rolling settlement-average line. That is only an anecdotal example of the visualization design; the script and its accuracy were not independently verified.

Build the comparison with defensible data

Begin with one ticker. Record its contract type, strike or band, full rules, named index, open and close times, expiration time, time zone, and official observation window. Do not begin with a generic “Bitcoin market” label and infer the contract later.

A chart-ready table should contain:

Field group Recommended columns
Time UTC timestamp, source timestamp, receipt timestamp
Bitcoin values Exchange midpoint, BRTI, provisional or final window mean
Kalshi market Yes bid, Yes ask, midpoint, last trade
Contract context Strike or band edges, window markers, expiration marker

Kalshi documents REST, WebSocket, and FIX interfaces for event-contract market data. Its API documentation identifies those interfaces and provides REST and WebSocket specifications. For a live chart, record timestamped quotes while the market is open rather than assuming they can be reconstructed afterward.

Use a fixed acquisition loop:

  1. Subscribe to or poll the selected exchange pair.
  2. Record exchange bid, ask, source timestamp, and receipt timestamp.
  3. Record BRTI with the benchmark timestamp.
  4. Record the selected Kalshi market’s Yes bid, Yes ask, and last trade.
  5. Write append-only rows to storage before transforming the data.
  6. Synchronize the collector’s clock and monitor drift.
  7. Convert every timestamp to UTC.
  8. Resample only after preserving the raw records.

For exchange data, calculate:

Exchange midpoint = best bid + best ask ÷ 2

Name that series precisely—for example, “Coinbase BTC-USD midpoint”—rather than “Bitcoin price.” If you use last trades instead, label the series accordingly.

For expired contracts, Kalshi’s historical-market endpoint accepts a ticker and returns one market record. Documented fields include bid, ask, last price, timing, volume, liquidity, strike boundaries, result, and settlement value. That page does not demonstrate a complete candle, trade-history, or quote-history series, so one final record cannot reconstruct every earlier bid, ask, or trade.

The following Python example joins previously recorded files and produces a two-axis chart. It assumes Kalshi prices have already been normalized to decimal values and that window_start and window_end were copied from the contract rules.

import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

TICKER = "KXBTCD-26SEP1022"       # Illustrative historical ticker
PAIR = "Coinbase BTC-USD midpoint"

WINDOW_START = pd.Timestamp("2026-09-10T21:59:01Z")
WINDOW_END = pd.Timestamp("2026-09-10T22:00:00Z")
MAX_STALENESS = pd.Timedelta("2s")

def load_csv(path):
    df = pd.read_csv(path, parse_dates=["timestamp"])
    df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
    return df.sort_values("timestamp").drop_duplicates("timestamp")

exchange = load_csv("exchange.csv")
brti = load_csv("brti.csv")
kalshi = load_csv("kalshi.csv")

exchange["exchange_mid"] = (exchange["bid"] + exchange["ask"]) / 2
kalshi["kalshi_mid"] = (kalshi["yes_bid"] + kalshi["yes_ask"]) / 2

# Preserve source times so stale observations remain detectable.
exchange = exchange.rename(columns={"timestamp": "exchange_source_time"})
kalshi = kalshi.rename(columns={"timestamp": "kalshi_source_time"})

start = min(
    exchange["exchange_source_time"].min(),
    brti["timestamp"].min(),
    kalshi["kalshi_source_time"].min()
).floor("s")

end = max(
    exchange["exchange_source_time"].max(),
    brti["timestamp"].max(),
    kalshi["kalshi_source_time"].max()
).ceil("s")

chart = pd.DataFrame({
    "timestamp": pd.date_range(start, end, freq="1s", tz="UTC")
})

# Use backward joins so future observations are never assigned to earlier times.
chart = pd.merge_asof(
    chart,
    exchange[["exchange_source_time", "exchange_mid"]],
    left_on="timestamp",
    right_on="exchange_source_time",
    direction="backward",
    tolerance=MAX_STALENESS
)

chart = pd.merge_asof(
    chart,
    kalshi[
        ["kalshi_source_time", "yes_bid", "yes_ask",
         "kalshi_mid", "last_price"]
    ],
    left_on="timestamp",
    right_on="kalshi_source_time",
    direction="backward",
    tolerance=MAX_STALENESS
)

# BRTI is aligned to exact one-second buckets and is not forward-filled.
brti_1s = (
    brti.set_index("timestamp")[["brti"]]
        .resample("1s")
        .last()
        .reset_index()
)

chart = chart.merge(brti_1s, on="timestamp", how="left")

# Use only observations inside the official rule window.
official_window = chart[
    chart["timestamp"].between(WINDOW_START, WINDOW_END)
].copy()

eligible = official_window.dropna(subset=["brti"])
eligible["provisional_window_mean"] = eligible["brti"].expanding().mean()

chart = chart.merge(
    eligible[["timestamp", "provisional_window_mean"]],
    on="timestamp",
    how="left"
)

# Do not publish a reconstructed final mean unless all 60 readings exist.
final_window_mean = None
if len(eligible) == 60:
    final_window_mean = eligible["brti"].mean()

fig = make_subplots(specs=[[{"secondary_y": True}]])

fig.add_trace(
    go.Scatter(
        x=chart["timestamp"],
        y=chart["exchange_mid"],
        name=PAIR
    ),
    secondary_y=False
)

fig.add_trace(
    go.Scatter(
        x=chart["timestamp"],
        y=chart["brti"],
        name="Live BRTI"
    ),
    secondary_y=False
)

fig.add_trace(
    go.Scatter(
        x=chart["timestamp"],
        y=chart["provisional_window_mean"],
        name="Provisional official-window mean"
    ),
    secondary_y=False
)

# Ask first, then bid with fill to create a visible spread band.
fig.add_trace(
    go.Scatter(
        x=chart["timestamp"],
        y=chart["yes_ask"] * 100,
        name="Kalshi Yes ask",
        line=dict(width=1)
    ),
    secondary_y=True
)

fig.add_trace(
    go.Scatter(
        x=chart["timestamp"],
        y=chart["yes_bid"] * 100,
        name="Kalshi Yes bid",
        fill="tonexty",
        line=dict(width=1)
    ),
    secondary_y=True
)

fig.add_vrect(
    x0=WINDOW_START,
    x1=WINDOW_END,
    fillcolor="gray",
    opacity=0.15,
    line_width=0,
    annotation_text="Official observation window"
)

if final_window_mean is not None:
    fig.add_hline(
        y=final_window_mean,
        line_dash="dash",
        annotation_text="Reconstructed final window mean",
        secondary_y=False
    )

fig.update_layout(
    title=f"{TICKER}: exchange, BRTI, and Kalshi probability",
    hovermode="x unified"
)

fig.update_yaxes(title_text="Bitcoin / benchmark value (USD)", secondary_y=False)
fig.update_yaxes(
    title_text="Kalshi Yes price (%)",
    range=[0, 100],
    secondary_y=True
)

fig.show()

The two-second backward-join tolerance in this example is a charting choice, not a settlement rule. Retain source times, calculate observation age, and disclose the tolerance. Do not silently forward-fill missing BRTI values, and do not treat repeated stale quotes as fresh market activity.

Publication notes should identify data sources, field definitions, exchange venue and pair, time-zone conversion, missing observations, resampling choices, API or display latency, clock synchronization, and whether the official rule window was independently reproduced. Do not call the result settlement-grade when observations are missing or when clocks, filters, and timing have not been verified.

Read probability and spot price without conflating them

A Kalshi Bitcoin event contract is a binary outcome claim. It does not represent ownership of Bitcoin, and its cent price is not a BTC/USD quote.

For a threshold contract such as “Will Bitcoin settle above K?”, read the contract price in relation to Bitcoin’s distance from the strike and the time remaining. The same spot price can accompany different market probabilities at different times because the unresolved path to expiration is different.

For a range contract, draw both band edges on the dollar panel. The relevant question is whether the final benchmark value lands inside the interval—not whether an exchange price entered the band earlier.

Contract structures also affect how outcomes relate. In a nested threshold ladder, several lower strikes can settle Yes when the final value is sufficiently high. In a mutually exclusive range ladder, one band ordinarily contains the final value. These structures and their settlement implications are explained in a Kalshi Bitcoin contract guide, but the current market’s official rules remain controlling.

A contract trading near 60 cents can be read as an approximate 60% market-implied probability, not an exact guarantee. Bid-ask spreads, fees, liquidity, stale trades, time value, resolution interpretation, and mispricing can separate the displayed price from a reasonable probability estimate. The Yes No Odds guide to how prediction-market prices become probabilities explains that mapping and its practical frictions.

This article is educational and is not trading advice.

Do not mix event contracts with Kalshi Bitcoin perpetuals

Kalshi’s Bitcoin perpetual is a separate product with a dollar-denominated chart and perpetual-market concepts such as leverage, funding, volume, and open interest. Kalshi describes periodic funding as a mechanism that adjusts the perpetual price toward an underlying index on its Bitcoin perpetual page.

Product Displayed value Primary interpretation
Bitcoin perpetual Dollar price Price of a leveraged, non-expiring derivatives position
Bitcoin event contract Cents or percentage Price of a defined binary outcome

If the subject is the perpetual, build a separate perpetual-versus-exchange chart. Put both dollar prices on compatible axes, then add basis or funding as a distinct series or panel. Never merge a perpetual price with an event-contract probability as if they were two versions of the same measurement.

Before publishing a Kalshi Bitcoin comparison:

  • Choose one ticker.
  • Confirm its benchmark, strike or band, expiration time, time zone, and settlement rules.
  • Record exchange, benchmark, and Kalshi source timestamps.
  • Plot exchange BTC/USD, BRTI, and the rule-specific benchmark average on a dollar axis.
  • Plot Kalshi bid and ask on a separate probability axis.
  • Mark the official observation window.
  • Keep provisional, trailing, reconstructed, and official settlement values distinct.
  • Disclose missing data, latency, resampling, staleness, and clock limitations.

A useful chart compares distinct measurements without pretending they are the same price.