use super::Kalshi;
use crate::kalshi_error::*;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
impl Kalshi {
#[allow(clippy::too_many_arguments)]
pub async fn get_markets(
&self,
limit: Option<i64>,
cursor: Option<String>,
event_ticker: Option<String>,
series_ticker: Option<String>,
status: Option<String>,
tickers: Option<String>,
min_close_ts: Option<i64>,
max_close_ts: Option<i64>,
min_created_ts: Option<i64>,
max_created_ts: Option<i64>,
min_settled_ts: Option<i64>,
max_settled_ts: Option<i64>,
mve_filter: Option<MveFilter>,
) -> Result<(Option<String>, Vec<Market>), KalshiError> {
let url = format!("{}/markets", self.base_url);
let mut p = vec![];
add_param!(p, "limit", limit);
add_param!(p, "cursor", cursor);
add_param!(p, "event_ticker", event_ticker);
add_param!(p, "series_ticker", series_ticker);
add_param!(p, "status", status);
add_param!(p, "tickers", tickers);
add_param!(p, "min_close_ts", min_close_ts);
add_param!(p, "max_close_ts", max_close_ts);
add_param!(p, "min_created_ts", min_created_ts);
add_param!(p, "max_created_ts", max_created_ts);
add_param!(p, "min_settled_ts", min_settled_ts);
add_param!(p, "max_settled_ts", max_settled_ts);
add_param!(p, "mve_filter", mve_filter);
let res: MarketListResponse = self
.client
.get(reqwest::Url::parse_with_params(&url, &p)?)
.send()
.await?
.json()
.await?;
Ok((res.cursor, res.markets))
}
pub async fn get_market(&self, ticker: &str) -> Result<Market, KalshiError> {
let url = format!("{}/markets/{}", self.base_url, ticker);
let res: SingleMarketResponse = self.client.get(url).send().await?.json().await?;
Ok(res.market)
}
pub async fn get_orderbook(
&self,
ticker: &str,
depth: Option<i32>,
) -> Result<Orderbook, KalshiError> {
let mut url = format!("{}/markets/{}/orderbook", self.base_url, ticker);
if let Some(d) = depth {
url.push_str(&format!("?depth={}", d));
}
let response = self.client.get(&url).send().await?;
let response_text = response.text().await?;
let json_value: serde_json::Value = serde_json::from_str(&response_text).map_err(|e| {
eprintln!(
"ERROR: Failed to parse response as JSON for ticker {}: {}",
ticker, e
);
eprintln!("ERROR: Raw response: {}", response_text);
KalshiError::UserInputError(format!("Failed to parse JSON: {}", e))
})?;
if !json_value.is_object() || !json_value.as_object().unwrap().contains_key("orderbook") {
eprintln!(
"ERROR: Response does not contain 'orderbook' field for ticker: {}",
ticker
);
eprintln!(
"ERROR: Available keys: {:?}",
json_value
.as_object()
.map(|obj| obj.keys().collect::<Vec<_>>())
);
eprintln!(
"ERROR: Full response: {}",
serde_json::to_string_pretty(&json_value).unwrap()
);
return Err(KalshiError::UserInputError(
"missing field `orderbook`".to_string(),
));
}
let res: OrderbookResponse = serde_json::from_value(json_value).map_err(|e| {
eprintln!(
"ERROR: Failed to deserialize OrderbookResponse for ticker {}: {}",
ticker, e
);
KalshiError::UserInputError(format!("Failed to deserialize: {}", e))
})?;
Ok(res.orderbook)
}
pub async fn get_orderbook_full(&self, ticker: &str) -> Result<Orderbook, KalshiError> {
self.get_orderbook(ticker, None).await
}
pub async fn get_market_candlesticks(
&self,
ticker: &str,
series_ticker: &str,
start_ts: Option<i64>,
end_ts: Option<i64>,
period_interval: Option<i32>,
) -> Result<Vec<Candle>, KalshiError> {
let url = format!(
"{}/series/{}/markets/{}/candlesticks",
self.base_url, series_ticker, ticker
);
let mut p = vec![];
add_param!(p, "start_ts", start_ts);
add_param!(p, "end_ts", end_ts);
add_param!(p, "period_interval", period_interval);
let res: CandlestickListResponse = self
.client
.get(reqwest::Url::parse_with_params(&url, &p)?)
.send()
.await?
.json()
.await?;
Ok(res.candlesticks)
}
pub async fn batch_get_market_candlesticks(
&self,
market_tickers: Vec<String>,
start_ts: i64,
end_ts: i64,
period_interval: i32,
include_latest_before_start: Option<bool>,
) -> Result<Vec<MarketCandlesticks>, KalshiError> {
if market_tickers.len() > 100 {
return Err(KalshiError::UserInputError(
"Maximum 100 market tickers allowed per batch request".to_string(),
));
}
let url = format!("{}/markets/candlesticks/batch", self.base_url);
let tickers_param = market_tickers.join(",");
let mut p = vec![
("market_tickers", tickers_param),
("start_ts", start_ts.to_string()),
("end_ts", end_ts.to_string()),
("period_interval", period_interval.to_string()),
];
if let Some(include_latest) = include_latest_before_start {
p.push(("include_latest_before_start", include_latest.to_string()));
}
let res: BatchCandlestickResponse = self
.client
.get(reqwest::Url::parse_with_params(&url, &p)?)
.send()
.await?
.json()
.await?;
Ok(res.markets)
}
pub async fn get_trades(
&self,
limit: Option<i64>,
cursor: Option<String>,
ticker: Option<String>,
min_ts: Option<i64>,
max_ts: Option<i64>,
) -> Result<(Option<String>, Vec<Trade>), KalshiError> {
let url = format!("{}/markets/trades", self.base_url);
let mut p = vec![];
add_param!(p, "limit", limit);
add_param!(p, "cursor", cursor);
add_param!(p, "ticker", ticker);
add_param!(p, "min_ts", min_ts);
add_param!(p, "max_ts", max_ts);
let res: TradeListResponse = self
.client
.get(reqwest::Url::parse_with_params(&url, &p)?)
.send()
.await?
.json()
.await?;
Ok((res.cursor, res.trades))
}
pub async fn get_series_list(
&self,
limit: Option<i64>,
cursor: Option<String>,
category: Option<String>,
tags: Option<String>,
) -> Result<(Option<String>, Vec<Series>), KalshiError> {
let mut p = Vec::new();
add_param!(p, "limit", limit);
add_param!(p, "cursor", cursor);
add_param!(p, "category", category);
add_param!(p, "tags", tags);
let path = if p.is_empty() {
"/series".to_string()
} else {
format!("/series?{}", serde_urlencoded::to_string(&p)?)
};
#[derive(Debug, serde::Deserialize)]
struct SeriesListResponse {
cursor: Option<String>,
series: Option<Vec<Series>>, }
let res: SeriesListResponse = self.signed_get(&path).await?;
Ok((res.cursor, res.series.unwrap_or_default()))
}
pub async fn get_series(&self, series_ticker: &str) -> Result<Series, KalshiError> {
let url = format!("{}/series/{}", self.base_url, series_ticker);
let res: SingleSeriesResponse = self.client.get(url).send().await?.json().await?;
Ok(res.series)
}
}
fn null_to_empty_vec<'de, D, T>(d: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de>,
{
let opt = Option::<Vec<T>>::deserialize(d)?;
Ok(opt.unwrap_or_default())
}
fn deserialize_dollar_levels<'de, D>(d: D) -> Result<Vec<(f32, i32)>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let opt = Option::<Vec<serde_json::Value>>::deserialize(d)?;
let Some(arr) = opt else {
return Ok(Vec::new());
};
let mut result = Vec::new();
for item in arr {
let level = item
.as_array()
.ok_or_else(|| Error::custom("Expected array for price level"))?;
if level.len() != 2 {
return Err(Error::custom("Expected array of length 2 for price level"));
}
let price: f32 = match &level[0] {
serde_json::Value::String(s) => s
.parse()
.map_err(|_| Error::custom(format!("Failed to parse price string: {}", s)))?,
serde_json::Value::Number(n) => n
.as_f64()
.ok_or_else(|| Error::custom("Failed to convert price number to f64"))?
as f32,
_ => return Err(Error::custom("Price must be string or number")),
};
let count: i32 = level[1]
.as_i64()
.ok_or_else(|| Error::custom("Count must be a number"))?
as i32;
result.push((price, count));
}
Ok(result)
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Event {
pub event_ticker: String,
pub series_ticker: String,
pub title: String,
pub sub_title: String,
pub mutually_exclusive: bool,
pub category: String,
pub strike_date: Option<String>,
pub strike_period: Option<String>,
pub markets: Option<Vec<Market>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Market {
pub ticker: String,
pub event_ticker: String,
pub market_type: String,
pub title: String,
pub subtitle: String,
pub yes_sub_title: String,
pub no_sub_title: String,
pub open_time: String,
pub close_time: String,
pub expected_expiration_time: Option<String>,
pub expiration_time: Option<String>,
pub latest_expiration_time: String,
pub settlement_timer_seconds: i64,
pub status: String,
pub response_price_units: String,
pub notional_value: i64,
pub tick_size: i64,
pub yes_bid: i64,
pub yes_ask: i64,
pub no_bid: i64,
pub no_ask: i64,
pub last_price: i64,
pub previous_yes_bid: i64,
pub previous_yes_ask: i64,
pub previous_price: i64,
pub volume: i64,
pub volume_24h: i64,
pub liquidity: i64,
pub open_interest: i64,
pub result: SettlementResult,
pub cap_strike: Option<f64>,
pub can_close_early: bool,
pub expiration_value: String,
pub category: String,
pub risk_limit_cents: i64,
pub strike_type: Option<String>,
pub floor_strike: Option<f64>,
pub rules_primary: String,
pub rules_secondary: String,
pub settlement_value: Option<String>,
pub functional_strike: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Series {
#[serde(default)]
pub ticker: Option<String>,
#[serde(default)]
pub frequency: Option<String>,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub category: Option<String>,
#[serde(default, deserialize_with = "null_to_empty_vec")]
pub tags: Vec<String>,
#[serde(default, deserialize_with = "null_to_empty_vec")]
pub settlement_sources: Vec<SettlementSource>,
#[serde(default)]
pub contract_url: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct MultivariateEventCollection {
pub collection_ticker: String,
pub title: String,
pub description: String,
pub category: String,
#[serde(default, deserialize_with = "null_to_empty_vec")]
pub tags: Vec<String>,
#[serde(default, deserialize_with = "null_to_empty_vec")]
pub markets: Vec<Market>,
pub created_time: String,
pub updated_time: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Candle {
pub start_ts: i64,
pub end_ts: i64,
pub yes_open: i32,
pub yes_high: i32,
pub yes_low: i32,
pub yes_close: i32,
pub no_open: i32,
pub no_high: i32,
pub no_low: i32,
pub no_close: i32,
pub volume: i64,
pub open_interest: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Orderbook {
pub yes: Option<Vec<Vec<i32>>>,
pub no: Option<Vec<Vec<i32>>>,
#[serde(default, deserialize_with = "deserialize_dollar_levels")]
pub yes_dollars: Vec<(f32, i32)>,
#[serde(default, deserialize_with = "deserialize_dollar_levels")]
pub no_dollars: Vec<(f32, i32)>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Snapshot {
pub yes_price: i32,
pub yes_bid: i32,
pub yes_ask: i32,
pub no_bid: i32,
pub no_ask: i32,
pub volume: i32,
pub open_interest: i32,
pub ts: i64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Trade {
pub trade_id: String,
pub taker_side: String,
pub ticker: String,
pub count: i32,
pub yes_price: i32,
pub no_price: i32,
pub created_time: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SettlementResult {
Yes,
No,
#[serde(rename = "")]
Void,
#[serde(rename = "all_no")]
AllNo,
#[serde(rename = "all_yes")]
AllYes,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MarketStatus {
Open,
Closed,
Settled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MveFilter {
Only,
Exclude,
}
impl std::fmt::Display for MveFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MveFilter::Only => write!(f, "only"),
MveFilter::Exclude => write!(f, "exclude"),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct MarketCandlesticks {
pub ticker: String,
pub candlesticks: Vec<Candle>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SettlementSource {
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct MarketListResponse {
cursor: Option<String>,
markets: Vec<Market>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)] struct SeriesListResponse {
cursor: Option<String>,
#[serde(default)]
series: Vec<Series>,
}
#[derive(Debug, Deserialize)]
struct TradeListResponse {
cursor: Option<String>,
trades: Vec<Trade>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)] struct CandlestickListResponse {
cursor: Option<String>,
candlesticks: Vec<Candle>,
}
#[derive(Debug, Deserialize)]
struct SingleMarketResponse {
market: Market,
}
#[derive(Debug, Deserialize)]
struct SingleSeriesResponse {
series: Series,
}
#[derive(Debug, Deserialize)]
struct OrderbookResponse {
orderbook: Orderbook,
}
#[derive(Debug, Deserialize)]
struct BatchCandlestickResponse {
markets: Vec<MarketCandlesticks>,
}