Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
467 changes: 0 additions & 467 deletions API_CHANGES_TODO.md

This file was deleted.

26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `subaccount` filter on `GetOrderGroupsParams` for per-subaccount order group
queries. New `get_order_group_for_subaccount()` method on `KalshiClient`.
- `market_id` (required) and `target_size_fp` (optional) fields on `IncentiveProgram`.
- `GetBalanceParams` with `subaccount` filter for `GET /portfolio/balance`. New
`get_balance_with_params()` method on `KalshiClient`. Omitting subaccount
returns combined balance across all subaccounts; `subaccount(0)` filters to
primary only.
- `settlement_value` field on `MarketLifecycleData` in WebSocket
`market_lifecycle_v2` channel. Present as a fixed-point dollar string on
`market_determined` events.
- Historical data endpoints (6 new endpoints):
- `get_historical_cutoff()` — cutoff timestamps for archived data
- `get_historical_markets()` / `get_historical_markets_with_params()` —
settled markets older than cutoff
- `get_historical_market(ticker)` — single historical market
- `get_historical_candlesticks(ticker, params)` — candlesticks for historical
markets
- `get_historical_fills()` / `get_historical_fills_with_params()` — archived
fills
- `get_historical_orders()` / `get_historical_orders_with_params()` — archived
orders
- New types: `HistoricalCutoffResponse`, `HistoricalCandlesticksResponse`,
`HistoricalCandlestick`, `HistoricalOhlc`, `HistoricalPriceOhlc`,
`GetHistoricalCandlesticksParams`, `GetHistoricalMarketsParams`,
`GetHistoricalFillsParams`, `GetHistoricalOrdersParams`.
- `yes_settlement_value_dollars` field on `MveSelectedLeg`.
- Tightened `IncentiveProgram` struct: `id`, `market_id`, `market_ticker`,
`incentive_type`, `start_date`, `end_date`, `period_reward`, and `paid_out`
are now non-optional to match the official Kalshi OpenAPI spec.
Expand Down Expand Up @@ -157,6 +180,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Fill.trade_id`, `Fill.market_ticker`, `Fill.ts` — legacy field names.
- `Event.category` — use series-level category instead.
- `MarketPosition.resting_orders_count` — deprecated by the API.
- `Market.liquidity` — always returns 0. Use orderbook data instead.
- `Market.liquidity_dollars` — always returns `"0.0000"`. Use orderbook data
instead.

## [0.2.0] - 2026-01-18

Expand Down
149 changes: 149 additions & 0 deletions examples/historical.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//! Historical Data API example
//!
//! Demonstrates the Historical Data API for fetching archived market data,
//! including cutoff timestamps, historical markets, candlesticks, fills, and orders.
//!
//! Run with: cargo run --example historical

use kalshi_trade_rs::{
CandlestickPeriod, GetHistoricalCandlesticksParams, GetHistoricalFillsParams,
GetHistoricalMarketsParams, GetHistoricalOrdersParams, KalshiClient, KalshiConfig,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();

let config = KalshiConfig::from_env()?;
println!("Connected to {:?} environment\n", config.environment);

let client = KalshiClient::new(config)?;

// 1. Get Historical Cutoff Timestamps
println!("=== Historical Cutoff ===");
let cutoff = client.get_historical_cutoff().await?;
println!("Markets archived through: {}", cutoff.market_settled_ts);
println!("Trades archived through: {}", cutoff.trades_created_ts);
println!("Orders archived through: {}", cutoff.orders_updated_ts);
println!();

// 2. Get Historical Markets (default params)
println!("=== Historical Markets (first 5) ===");
let params = GetHistoricalMarketsParams::new().limit(5);
let markets = client.get_historical_markets_with_params(params).await?;
println!("Returned {} historical markets", markets.markets.len());

for market in &markets.markets {
println!(
" {} | {:?} | result: {:?} | vol: {}",
market.ticker,
market.status,
market.result,
market.volume.unwrap_or(0)
);
}

if let Some(cursor) = &markets.cursor {
println!(" Next cursor: {}...", &cursor[..cursor.len().min(20)]);
}
println!();

// 3. Get a specific historical market and its candlesticks
if let Some(market) = markets.markets.first() {
let ticker = &market.ticker;

println!("=== Historical Market Detail: {} ===", ticker);
let detail = client.get_historical_market(ticker).await?;
let m = &detail.market;

println!(" Title: {}", m.title.as_deref().unwrap_or("(none)"));
println!(" Event: {}", m.event_ticker);
println!(" Type: {:?}", m.market_type);
println!(" Status: {:?}", m.status);
if let Some(sv) = &m.settlement_value_dollars {
println!(" Settlement value: ${}", sv);
}
println!();

// 4. Get Historical Candlesticks
// Use a broad time range to catch data for archived markets
println!("=== Historical Candlesticks: {} ===", ticker);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs() as i64;
let one_year_ago = now - 365 * 86400;

let params =
GetHistoricalCandlesticksParams::new(one_year_ago, now, CandlestickPeriod::OneDay);
match client.get_historical_candlesticks(ticker, params).await {
Ok(candles) => {
println!(" {} candlesticks returned", candles.candlesticks.len());
for candle in candles.candlesticks.iter().take(3) {
let close = candle
.price
.as_ref()
.and_then(|p| p.close.as_deref())
.unwrap_or("N/A");
println!(
" ts={} close=${} vol={}",
candle.end_period_ts, close, candle.volume
);
}
if candles.candlesticks.len() > 3 {
println!(" ... and {} more", candles.candlesticks.len() - 3);
}
}
Err(e) => println!(" Could not fetch candlesticks: {}", e),
}
println!();
}

// 5. Get Historical Fills (requires auth)
println!("=== Historical Fills (first 5) ===");
let params = GetHistoricalFillsParams::new().limit(5);
let fills = client.get_historical_fills_with_params(params).await?;
println!("Returned {} historical fills", fills.fills.len());

for fill in &fills.fills {
println!(
" {} {} {} {} @ {} ({})",
fill.ticker,
fill.action,
fill.count_fp,
fill.side,
fill.yes_price_fixed,
if fill.is_taker { "taker" } else { "maker" },
);
}

if fills.fills.is_empty() {
println!(" (No historical fills found)");
}
println!();

// 6. Get Historical Orders (requires auth)
println!("=== Historical Orders (first 5) ===");
let params = GetHistoricalOrdersParams::new().limit(5);
let orders = client.get_historical_orders_with_params(params).await?;
println!("Returned {} historical orders", orders.orders.len());

for order in &orders.orders {
println!(
" {} {} {} {} | {:?} | filled: {}/{}",
order.ticker,
order.action,
order.initial_count,
order.side,
order.status,
order.fill_count,
order.initial_count,
);
}

if orders.orders.is_empty() {
println!(" (No historical orders found)");
}

println!("\n=== Done ===");
Ok(())
}
25 changes: 21 additions & 4 deletions examples/portfolio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//! Run with: cargo run --example portfolio

use kalshi_trade_rs::{
GetFillsParams, GetOrdersParams, GetPositionsParams, GetSettlementsParams, KalshiClient,
KalshiConfig, OrderStatus, cents_to_dollars,
GetBalanceParams, GetFillsParams, GetOrdersParams, GetPositionsParams, GetSettlementsParams,
KalshiClient, KalshiConfig, OrderStatus, cents_to_dollars,
};

#[tokio::main]
Expand All @@ -21,8 +21,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

let client = KalshiClient::new(config)?;

// 1. Get Balance
println!("=== Balance ===");
// 1. Get Balance (combined across all subaccounts)
println!("=== Balance (all subaccounts) ===");

let balance = client.get_balance().await?;

Expand All @@ -35,6 +35,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

println!();

// 1b. Get Balance for primary account only
println!("=== Balance (primary account only) ===");

let params = GetBalanceParams::new().subaccount(0);
let primary_balance = client.get_balance_with_params(params).await?;

println!(
"Primary Available: ${:.2}",
cents_to_dollars(primary_balance.balance)
);
println!(
"Primary Portfolio Value: ${:.2}",
cents_to_dollars(primary_balance.portfolio_value)
);

println!();

// 2. Get Positions
println!("=== Positions ===");

Expand Down
1 change: 1 addition & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub(crate) mod communications;
pub(crate) mod events;
pub(crate) mod exchange;
pub(crate) mod fcm;
pub(crate) mod historical;
pub(crate) mod incentive_programs;
pub(crate) mod live_data;
pub(crate) mod markets;
Expand Down
20 changes: 19 additions & 1 deletion src/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Complete reference for all Kalshi REST API endpoints supported by this library.
| Structured Targets | 2 | 100% |
| Incentive Programs | 1 | 100% |
| FCM | 2 | 100% |
| **Total** | **78** | **100%** |
| Historical | 6 | 100% |
| **Total** | **84** | **100%** |

---

Expand Down Expand Up @@ -283,6 +284,23 @@ Specialized endpoints for FCM (Futures Commission Merchant) members.

---

## Historical API

Endpoints for accessing archived historical data past the cutoff timestamp.

| Status | Method | Endpoint | Rust Function | Notes |
|--------|--------|----------|---------------|-------|
| 🔲 | GET | `/historical/cutoff` | `get_historical_cutoff()` | No auth required |
| 🔲 | GET | `/historical/markets` | `get_historical_markets()` | No auth required |
| 🔲 | GET | `/historical/markets/{ticker}` | `get_historical_market()` | No auth required |
| 🔲 | GET | `/historical/markets/{ticker}/candlesticks` | `get_historical_candlesticks()` | No auth; returns `HistoricalCandlesticksResponse` |
| 🔲 | GET | `/historical/fills` | `get_historical_fills()` | Requires auth |
| 🔲 | GET | `/historical/orders` | `get_historical_orders()` | Requires auth |

**Source**: `src/api/historical.rs`

---

## Base URLs

| Environment | REST API | WebSocket |
Expand Down
73 changes: 73 additions & 0 deletions src/api/historical.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Historical data API endpoints.
//!
//! These endpoints provide access to historical market data that has been
//! archived past the cutoff timestamp. Cutoff, markets, and candlestick
//! endpoints do not require authentication. Fills and orders require auth.

use url::form_urlencoded;

use crate::{
client::HttpClient,
error::Result,
models::{
FillsResponse, GetHistoricalCandlesticksParams, GetHistoricalFillsParams,
GetHistoricalMarketsParams, GetHistoricalOrdersParams, HistoricalCandlesticksResponse,
HistoricalCutoffResponse, MarketResponse, MarketsResponse, OrdersResponse,
},
};

fn encode_ticker(ticker: &str) -> String {
form_urlencoded::byte_serialize(ticker.as_bytes()).collect()
}

/// Returns the cutoff timestamps for historical data.
pub async fn get_historical_cutoff(http: &HttpClient) -> Result<HistoricalCutoffResponse> {
http.get("/historical/cutoff").await
}

/// Returns historical markets matching the provided query parameters.
pub async fn get_historical_markets(
http: &HttpClient,
params: GetHistoricalMarketsParams,
) -> Result<MarketsResponse> {
let path = format!("/historical/markets{}", params.to_query_string());
http.get(&path).await
}

/// Returns a specific historical market by ticker.
pub async fn get_historical_market(http: &HttpClient, ticker: &str) -> Result<MarketResponse> {
let path = format!("/historical/markets/{}", encode_ticker(ticker));
http.get(&path).await
}

/// Returns historical candlestick data for a market.
pub async fn get_historical_candlesticks(
http: &HttpClient,
ticker: &str,
params: GetHistoricalCandlesticksParams,
) -> Result<HistoricalCandlesticksResponse> {
let path = format!(
"/historical/markets/{}/candlesticks{}",
encode_ticker(ticker),
params.to_query_string()
);
http.get(&path).await
}

/// Returns historical fills.
pub async fn get_historical_fills(
http: &HttpClient,
params: GetHistoricalFillsParams,
) -> Result<FillsResponse> {
let path = format!("/historical/fills{}", params.to_query_string());
http.get(&path).await
}

/// Returns historical orders.
pub async fn get_historical_orders(
http: &HttpClient,
params: GetHistoricalOrdersParams,
) -> Result<OrdersResponse> {
let path = format!("/historical/orders{}", params.to_query_string());
http.get(&path).await
}
10 changes: 6 additions & 4 deletions src/api/portfolio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ use crate::{
client::HttpClient,
error::Result,
models::{
BalanceResponse, FillsResponse, GetFillsParams, GetOrdersParams, GetPositionsParams,
GetSettlementsParams, OrdersResponse, PositionsResponse, SettlementsResponse,
BalanceResponse, FillsResponse, GetBalanceParams, GetFillsParams, GetOrdersParams,
GetPositionsParams, GetSettlementsParams, OrdersResponse, PositionsResponse,
SettlementsResponse,
},
};

/// Returns the available balance and portfolio value in cents.
pub async fn get_balance(http: &HttpClient) -> Result<BalanceResponse> {
http.get("/portfolio/balance").await
pub async fn get_balance(http: &HttpClient, params: GetBalanceParams) -> Result<BalanceResponse> {
let path = format!("/portfolio/balance{}", params.to_query_string());
http.get(&path).await
}

pub async fn get_positions(
Expand Down
Loading