use std::sync::Arc;
use crate::api::paths;
use crate::client::RobinhoodClient;
use crate::models::futures::*;
use crate::pagination::ResultsResponse;
use crate::{Result, RhoodError};
impl RobinhoodClient {
pub async fn get_futures_contract(&self, symbol: &str) -> Result<FuturesContract> {
let url = format!(
"{}symbol/{}/",
self.api_url(paths::FUTURES_CONTRACTS),
symbol.to_uppercase()
);
let wrapper: FuturesContractWrapper = self.get_futures(&url).await?;
Ok(wrapper.result)
}
pub async fn cached_futures_contract(&self, symbol: &str) -> Result<Arc<FuturesContract>> {
let key = symbol.to_uppercase();
if !self.resolvers.enabled {
return Ok(Arc::new(self.get_futures_contract(&key).await?));
}
if let Some(hit) = self.resolvers.futures_contracts.get(&key).await {
return Ok(hit);
}
let contract = self.get_futures_contract(&key).await?;
let wrapped = Arc::new(contract);
self.resolvers
.futures_contracts
.insert(key, wrapped.clone())
.await;
Ok(wrapped)
}
pub async fn get_futures_quote(&self, symbol: &str) -> Result<FuturesQuote> {
let contract = self.cached_futures_contract(symbol).await?;
let instrument_id = contract
.id
.clone()
.ok_or_else(|| RhoodError::InvalidSymbol(symbol.to_string()))?;
let params = [("ids", instrument_id.as_str())];
let wrapper: FuturesQuoteDataWrapper = self
.get_futures_with_params(&self.api_url(paths::FUTURES_QUOTES), ¶ms)
.await?;
wrapper
.data
.into_iter()
.next()
.map(|item| item.data)
.ok_or_else(|| RhoodError::InvalidSymbol(symbol.to_string()))
}
pub async fn get_futures_quotes(&self, symbols: &[&str]) -> Result<Vec<FuturesQuote>> {
let resolved_ids: Vec<String> =
::futures::future::try_join_all(symbols.iter().map(|symbol| async move {
let contract = self.cached_futures_contract(symbol).await?;
contract
.id
.clone()
.ok_or_else(|| RhoodError::InvalidSymbol((*symbol).to_string()))
}))
.await?;
let joined_ids = resolved_ids.join(",");
let params = [("ids", joined_ids.as_str())];
let wrapper: FuturesQuoteDataWrapper = self
.get_futures_with_params(&self.api_url(paths::FUTURES_QUOTES), ¶ms)
.await?;
Ok(wrapper.data.into_iter().map(|item| item.data).collect())
}
pub async fn get_futures_account_id(&self) -> Result<Option<String>> {
let resp: ResultsResponse<FuturesAccount> = self
.get_futures(&self.api_url(paths::FUTURES_ACCOUNTS))
.await?;
Ok(resp
.results
.into_iter()
.find(|account| account.account_type.as_deref() == Some("FUTURES"))
.and_then(|account| account.id))
}
pub async fn cached_futures_account_id(&self) -> Result<String> {
if !self.resolvers.enabled {
return self
.get_futures_account_id()
.await?
.ok_or_else(|| RhoodError::InvalidParameter("No futures account found".into()));
}
let cached = self
.resolvers
.futures_account_id
.get_or_try_init(|| async {
self.get_futures_account_id()
.await?
.ok_or_else(|| RhoodError::InvalidParameter("No futures account found".into()))
})
.await?;
Ok(cached.clone())
}
pub async fn get_all_futures_orders(&self, since: Option<&str>) -> Result<Vec<FuturesOrder>> {
let account_id = self.cached_futures_account_id().await?;
let url = format!(
"{}{account_id}/orders",
self.api_url(paths::FUTURES_ACCOUNTS)
);
let mut params: Vec<(&str, &str)> = vec![("contractType", "OUTRIGHT")];
if let Some(date) = since {
params.push(("updated_at[gte]", date));
}
self.get_futures_cursor_paginated(&url, ¶ms).await
}
}
#[cfg(test)]
mod tests {
use crate::models::futures::{
FuturesContract, FuturesContractWrapper, FuturesOrder, FuturesQuoteDataWrapper,
};
#[test]
fn futures_contract_deserializes() {
let json = r#"{
"id": "c60db22e-536d-43b4-9083-17717de8d217",
"symbol": "/ESH26:XCME",
"displaySymbol": "/ESH26",
"description": "E-mini S&P 500 Mar 2026",
"multiplier": "50",
"expiration": "2026-03-21",
"tradability": "tradable",
"state": "active"
}"#;
let contract: FuturesContract = serde_json::from_str(json).unwrap();
assert_eq!(
contract.id.as_deref(),
Some("c60db22e-536d-43b4-9083-17717de8d217")
);
assert_eq!(contract.symbol.as_deref(), Some("/ESH26:XCME"));
assert_eq!(contract.display_symbol.as_deref(), Some("/ESH26"));
assert_eq!(contract.multiplier.as_deref(), Some("50"));
}
#[test]
fn futures_quote_accepts_integer_sizes_from_live_wire() {
let wire = r#"{
"status":"SUCCESS",
"data":[{"status":"SUCCESS","data":{
"ask_price":"7164.25","ask_size":1,
"ask_venue_timestamp":"2026-04-17T17:00:00.12674-04:00",
"bid_price":"7163.75","bid_size":5,
"bid_venue_timestamp":"2026-04-17T17:00:00.12674-04:00",
"last_trade_price":"7164.25","last_trade_size":1,
"last_trade_venue_timestamp":"2026-04-17T16:59:58.64959-04:00",
"symbol":"/ESM26:XCME",
"instrument_id":"e7b95e72-9aa2-4779-89e1-c404163799ed",
"state":"active",
"updated_at":"2026-04-17T17:00:00.12674-04:00",
"out_of_band":false
}}]
}"#;
let wrapper: FuturesQuoteDataWrapper = serde_json::from_str(wire).unwrap();
let quote = &wrapper.data[0].data;
assert_eq!(quote.ask_size, Some(1));
assert_eq!(quote.bid_size, Some(5));
assert_eq!(quote.last_trade_size, Some(1));
assert_eq!(quote.ask_price.as_deref(), Some("7164.25"));
assert_eq!(quote.bid_price.as_deref(), Some("7163.75"));
assert_eq!(
quote.instrument_id.as_deref(),
Some("e7b95e72-9aa2-4779-89e1-c404163799ed")
);
assert_eq!(quote.state.as_deref(), Some("active"));
}
#[test]
fn futures_order_deserializes() {
let json = r#"{
"orderId": "order-123",
"orderState": "FILLED",
"quantity": "1",
"filledQuantity": "1",
"averagePrice": "6903.50",
"orderLegs": [{"side": "BUY"}],
"realizedPnl": {"realizedPnl": {"amount": "-50.00", "currency": "USD"}},
"totalFee": {"amount": "3.10", "currency": "USD"},
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-01-15T10:01:00Z"
}"#;
let order: FuturesOrder = serde_json::from_str(json).unwrap();
assert_eq!(order.order_id.as_deref(), Some("order-123"));
assert_eq!(order.order_state.as_deref(), Some("FILLED"));
assert_eq!(order.filled_quantity.as_deref(), Some("1"));
assert!(order.realized_pnl.is_some());
assert!(order.total_fee.is_some());
}
#[test]
fn futures_contract_wrapper_deserializes() {
let json = r#"{"result": {"id": "abc", "symbol": "/ESH26:XCME"}}"#;
let wrapper: FuturesContractWrapper = serde_json::from_str(json).unwrap();
assert_eq!(wrapper.result.id.as_deref(), Some("abc"));
}
#[test]
fn futures_quote_data_wrapper_deserializes() {
let json = r#"{"data": [{"data": {"bid_price": "100", "ask_price": "101"}}]}"#;
let wrapper: FuturesQuoteDataWrapper = serde_json::from_str(json).unwrap();
assert_eq!(wrapper.data.len(), 1);
assert_eq!(wrapper.data[0].data.bid_price.as_deref(), Some("100"));
}
}