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
587 changes: 587 additions & 0 deletions API_CHANGES_TODO.md

Large diffs are not rendered by default.

36 changes: 29 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
on `Rfq` and `Quote` response types.
- Partial acceptance support: `contracts` field on `AcceptQuoteRequest` and
`CreateQuoteRequest`.
- Fixed-point `_fp` fields on REST API types: `count_fp` on `CreateOrderRequest`
and `AmendOrderRequest`, `reduce_by_fp`/`reduce_to_fp` on
`DecreaseOrderRequest`, `queue_position_fp` on `QueuePosition` and
`OrderQueuePositionResponse`, `contracts_fp` on `CreateRfqRequest`/`Rfq`/`Quote`,
`contracts_limit_fp` on `CreateOrderGroupRequest`/`UpdateOrderGroupLimitRequest`/
`GetOrderGroupResponse`/`OrderGroupSummary`.
- Fixed-point `_fp` fields on REST API response types: `count_fp` on `Fill` and
`Trade`; `fill_count_fp`/`remaining_count_fp`/`initial_count_fp` on `Order`;
`yes_count_fp`/`no_count_fp` on `Settlement`; `position_fp`/
`resting_orders_count_fp` on `MarketPosition`; `total_cost_shares_fp` on
`EventPosition`; `volume_fp`/`open_interest_fp` on `Market` and `Candlestick`;
`queue_position_fp` on `QueuePosition` and `OrderQueuePositionResponse`;
`contracts_fp` on `Rfq`/`Quote`; `contracts_limit_fp` on
`GetOrderGroupResponse`/`OrderGroupSummary`; `volume_fp` on `SeriesVolume`.
Request types: `count_fp` on `CreateOrderRequest`/`AmendOrderRequest`,
`reduce_by_fp`/`reduce_to_fp` on `DecreaseOrderRequest`, `contracts_fp` on
`CreateRfqRequest`, `contracts_limit_fp` on `CreateOrderGroupRequest`/
`UpdateOrderGroupLimitRequest`.
- Fixed-point `_fp` fields on WebSocket message types: `delta_fp` on
`OrderbookDeltaData`, `volume_fp`/`open_interest_fp` on `TickerData`,
`count_fp` on `TradeData`, `count_fp`/`post_position_fp` on `FillData`,
Expand All @@ -57,7 +63,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
update time.
- `name` and `subaccount` fields on `CreateOrderGroupRequest`; `name`, `status`,
`created_time` on `OrderGroupSummary`.
- `fee_cost` on `Fill` for exchange fee cost as a fixed-point dollar string.
- `fee_cost` on `Fill` and `Settlement` for exchange fee cost as a fixed-point
dollar string.
- `fee_cost` on WebSocket `FillData` for exchange fee cost.
- `subaccount` filter on `GetSettlementsParams` and `GetFillsParams` for
per-subaccount settlement and fill queries.
- `market_result` and `total_cost` on `MarketPosition`.
- `status` field on `Event`, `Milestone`, and `StructuredTarget`.
- Series metadata fields: `category`, `status`, `tags`, `settlement_sources`
Expand Down Expand Up @@ -102,6 +112,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
aggregator now guarantees that summaries are only produced for fully
initialized orderbooks, making the field redundant. Remove any
`summary.initialized` checks from your code.
- **Breaking:** `SubaccountBalance::balance` changed from `i64` (centicents) to
`String` (fixed-point dollar string, e.g. `"500.0000"`). The
`balance_dollars()` helper has been removed — the balance is now already in
dollars. `updated_ts` changed from `Option<i64>` to `i64` (now always present
in the API response).
- **Breaking:** Several fields promoted from `Option<String>` to `String` to
match the official API spec: `Fill::count_fp`, `Fill::yes_price_fixed`,
`Fill::no_price_fixed`, `Fill::fee_cost`, `Settlement::yes_count_fp`,
`Settlement::no_count_fp`, `Settlement::fee_cost`, `Trade::count_fp`,
`Trade::yes_price_dollars`, `Trade::no_price_dollars`.
- **Breaking:** `Fill::created_time` changed from `String` to `Option<String>`
(optional per the API spec).
- `CreateRfqRequest::with_target_cost_dollars()` now sends the dollar amount via
the `target_cost_dollars` field instead of converting to centi-cents.
- `Channel::requires_market_ticker()` now returns `true` only for
Expand Down
14 changes: 7 additions & 7 deletions examples/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

for trade in &trades.trades {
println!(
" {} | {} contracts @ ${:.2} YES | taker: {:?} | {}",
" {} | {} contracts @ {} YES | taker: {:?} | {}",
trade.ticker,
trade.count,
cents_to_dollars(trade.yes_price),
trade.count_fp,
trade.yes_price_dollars,
trade.taker_side,
trade.created_time
);
Expand All @@ -189,10 +189,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} else {
for trade in &market_trades.trades {
println!(
" {} contracts @ ${:.2} YES (${:.2} NO) | {:?}",
trade.count,
cents_to_dollars(trade.yes_price),
cents_to_dollars(trade.no_price),
" {} contracts @ {} YES ({} NO) | {:?}",
trade.count_fp,
trade.yes_price_dollars,
trade.no_price_dollars,
trade.taker_side
);
}
Expand Down
17 changes: 10 additions & 7 deletions examples/portfolio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

for fill in &fills.fills {
println!(
" {} {} {} {} @ ${:.2} ({})",
" {} {} {} {} @ {} ({}, fee: {})",
fill.ticker,
fill.action,
fill.count,
fill.count_fp,
fill.side,
cents_to_dollars(fill.yes_price),
if fill.is_taker { "taker" } else { "maker" }
fill.yes_price_fixed,
if fill.is_taker { "taker" } else { "maker" },
fill.fee_cost,
);
}
println!();
Expand Down Expand Up @@ -161,14 +162,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

for settlement in settlements.settlements.iter().take(5) {
let ticker = &settlement.ticker;
let pnl = settlement.revenue;
let result = format!("{:?}", settlement.market_result);

println!(
" {} | result: {} | P&L: ${:.2}",
" {} | result: {} | P&L: ${:.2} | fee: {} | yes: {} no: {}",
ticker,
result,
cents_to_dollars(pnl)
cents_to_dollars(settlement.revenue),
settlement.fee_cost,
settlement.yes_count_fp,
settlement.no_count_fp,
);
}

Expand Down
8 changes: 4 additions & 4 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1762,10 +1762,10 @@ impl KalshiClient {
///
/// ```ignore
/// let balances = client.get_subaccount_balances().await?;
/// for balance in balances.balances {
/// println!("Subaccount {}: ${:.2}",
/// balance.subaccount_id,
/// balance.balance_dollars()
/// for balance in balances.subaccount_balances {
/// println!("Subaccount {}: {}",
/// balance.subaccount_number,
/// balance.balance
/// );
/// }
/// ```
Expand Down
17 changes: 7 additions & 10 deletions src/models/fill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,30 @@ pub struct Fill {
pub side: Side,
pub action: Action,
pub count: i64,
/// Count (fixed-point decimal string).
#[serde(default)]
pub count_fp: Option<String>,
/// Count (fixed-point decimal string, e.g. `"10.00"`).
pub count_fp: String,
/// Price in cents.
pub yes_price: i64,
/// Price in cents.
pub no_price: i64,
/// Price in fixed-point dollars.
#[serde(default)]
pub yes_price_fixed: Option<String>,
pub yes_price_fixed: String,
/// Price in fixed-point dollars.
#[serde(default)]
pub no_price_fixed: Option<String>,
pub no_price_fixed: String,
/// Whether this fill removed liquidity.
pub is_taker: bool,
#[serde(default)]
pub client_order_id: Option<String>,
pub created_time: String,
#[serde(default)]
pub created_time: Option<String>,
/// Deprecated: legacy Unix timestamp field.
#[serde(default)]
pub ts: Option<i64>,
/// Deprecated: use `yes_price` or `no_price` instead.
#[serde(default)]
pub price: Option<f64>,
/// Exchange fee cost as a fixed-point dollar string.
#[serde(default)]
pub fee_cost: Option<String>,
pub fee_cost: String,
/// Subaccount number this fill belongs to (0 for primary, 1-32 for subaccounts).
#[serde(default)]
pub subaccount_number: Option<i32>,
Expand Down
19 changes: 9 additions & 10 deletions src/models/market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,19 +677,16 @@ pub struct Trade {
pub price: Option<f64>,
/// Contract quantity.
pub count: i64,
/// Contract quantity (fixed-point decimal string).
#[serde(default)]
pub count_fp: Option<String>,
/// Contract quantity (fixed-point decimal string, e.g. `"10.00"`).
pub count_fp: String,
/// Yes side price in cents.
pub yes_price: i64,
/// No side price in cents.
pub no_price: i64,
/// Yes price in dollars.
#[serde(default)]
pub yes_price_dollars: Option<String>,
/// No price in dollars.
#[serde(default)]
pub no_price_dollars: Option<String>,
/// Yes price in fixed-point dollars.
pub yes_price_dollars: String,
/// No price in fixed-point dollars.
pub no_price_dollars: String,
pub taker_side: TakerSide,
pub created_time: String,
}
Expand Down Expand Up @@ -1278,13 +1275,15 @@ mod tests {
"count_fp": "10.5",
"yes_price": 50,
"no_price": 50,
"yes_price_dollars": "0.50",
"no_price_dollars": "0.50",
"taker_side": "yes",
"created_time": "2025-01-10T12:00:00Z"
}"#;
let trade: Trade = serde_json::from_str(json).unwrap();
assert_eq!(trade.trade_id, "abc123");
assert_eq!(trade.count, 10);
assert_eq!(trade.count_fp, Some("10.5".to_string()));
assert_eq!(trade.count_fp, "10.5");
assert_eq!(trade.taker_side, TakerSide::Yes);
}

Expand Down
28 changes: 19 additions & 9 deletions src/models/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,22 @@ pub struct Settlement {
pub market_result: MarketResult,
/// Number of YES contracts held at settlement.
pub yes_count: i64,
/// Number of YES contracts (fixed-point decimal string).
#[serde(default)]
pub yes_count_fp: Option<String>,
/// Number of YES contracts (fixed-point decimal string, e.g. `"10.00"`).
pub yes_count_fp: String,
/// Total cost of YES contracts in cents.
pub yes_total_cost: i64,
/// Number of NO contracts held at settlement.
pub no_count: i64,
/// Number of NO contracts (fixed-point decimal string).
#[serde(default)]
pub no_count_fp: Option<String>,
/// Number of NO contracts (fixed-point decimal string, e.g. `"10.00"`).
pub no_count_fp: String,
/// Total cost of NO contracts in cents.
pub no_total_cost: i64,
/// Revenue from settlement in cents.
pub revenue: i64,
/// Settlement timestamp.
pub settled_time: String,
/// Fee cost in dollars.
#[serde(default)]
pub fee_cost: Option<String>,
/// Fee cost as a fixed-point dollar string.
pub fee_cost: String,
/// Settlement value in cents.
#[serde(default)]
pub value: Option<i64>,
Expand Down Expand Up @@ -69,6 +66,9 @@ pub struct GetSettlementsParams {
/// Filter items before this Unix timestamp.
#[serde(skip_serializing_if = "Option::is_none")]
pub max_ts: Option<i64>,
/// Filter by subaccount number (0 for primary, 1-32 for subaccounts).
#[serde(skip_serializing_if = "Option::is_none")]
pub subaccount: Option<i32>,
}

impl GetSettlementsParams {
Expand Down Expand Up @@ -143,6 +143,15 @@ impl GetSettlementsParams {
self
}

/// Filter by subaccount number.
///
/// Use 0 for the primary account, or 1-32 for numbered subaccounts.
#[must_use]
pub fn subaccount(mut self, subaccount: i32) -> Self {
self.subaccount = Some(subaccount);
self
}

#[must_use]
pub fn to_query_string(&self) -> String {
let mut qb = QueryBuilder::new();
Expand All @@ -152,6 +161,7 @@ impl GetSettlementsParams {
qb.push_opt("event_ticker", self.event_ticker.as_ref());
qb.push_opt("min_ts", self.min_ts);
qb.push_opt("max_ts", self.max_ts);
qb.push_opt("subaccount", self.subaccount);
qb.build()
}
}
Expand Down
37 changes: 10 additions & 27 deletions src/models/subaccount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,30 +172,15 @@ pub struct SubaccountBalancesResponse {

/// Balance for a single subaccount.
///
/// **Note:** This endpoint returns balance in **centicents** (1/100th of a cent),
/// which differs from `GET /portfolio/balance` which returns balance in cents.
/// Use [`balance_dollars()`](Self::balance_dollars) for convenient conversion.
/// The `balance` field is a fixed-point dollar string (e.g., `"500.0000"`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubaccountBalance {
/// Subaccount number (0 for primary, 1-32 for subaccounts).
pub subaccount_number: i32,
/// Available balance in centicents (1/100th of a cent).
/// Divide by 10,000 to get dollars.
pub balance: i64,
/// Available balance as a fixed-point dollar string.
pub balance: String,
/// Last update timestamp (Unix seconds).
#[serde(default)]
pub updated_ts: Option<i64>,
}

impl SubaccountBalance {
/// Returns the balance in dollars.
///
/// Converts from centicents (API unit) to dollars by dividing by 10,000.
#[inline]
#[must_use]
pub fn balance_dollars(&self) -> f64 {
self.balance as f64 / 10000.0
}
pub updated_ts: i64,
}

/// Query parameters for GET /portfolio/subaccounts/transfers.
Expand Down Expand Up @@ -333,14 +318,12 @@ mod tests {
}

#[test]
fn test_balance_dollars() {
// Balance is in centicents (1/100th of a cent), so 5000000 centicents = $500
let balance = SubaccountBalance {
subaccount_number: 0,
balance: 5000000,
updated_ts: None,
};
assert!((balance.balance_dollars() - 500.0).abs() < f64::EPSILON);
fn test_balance_deserialization() {
let json = r#"{"subaccount_number": 0, "balance": "500.0000", "updated_ts": 1706400000}"#;
let balance: SubaccountBalance = serde_json::from_str(json).unwrap();
assert_eq!(balance.subaccount_number, 0);
assert_eq!(balance.balance, "500.0000");
assert_eq!(balance.updated_ts, 1706400000);
}

#[test]
Expand Down
3 changes: 3 additions & 0 deletions src/ws/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ pub struct FillData {
/// Side that was purchased.
#[serde(skip_serializing_if = "Option::is_none")]
pub purchased_side: Option<Side>,
/// Exchange fee cost as a fixed-point dollar string.
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_cost: Option<String>,
}

/// Market position data for user position updates.
Expand Down