use chrono::NaiveDate;
use crate::types::instrument::InstrumentType;
use crate::types::order::{Order, OrderLeg};
pub const CRYPTOCURRENCY_TRADING_SUSPENDED_ON: NaiveDate =
match NaiveDate::from_ymd_opt(2026, 6, 29) {
Some(date) => date,
None => panic!("2026-06-29 is a real date"),
};
pub const CRYPTOCURRENCY_TRADING_SOURCE: &str = "https://developer.tastytrade.com/release-notes/";
pub const CRYPTOCURRENCY_TRADING_ENABLED: bool = false;
pub(crate) fn ensure_legs_are_tradable(legs: &[OrderLeg]) -> crate::TastyResult<()> {
if CRYPTOCURRENCY_TRADING_ENABLED {
return Ok(());
}
if legs
.iter()
.any(|leg| *leg.instrument_type() == InstrumentType::Cryptocurrency)
{
return Err(crate::TastyTradeError::Precondition(format!(
"tastytrade disabled cryptocurrency trading through the API on {}, so this \
order cannot route; instrument data and market data are unaffected. \
Source: {}",
CRYPTOCURRENCY_TRADING_SUSPENDED_ON, CRYPTOCURRENCY_TRADING_SOURCE
)));
}
Ok(())
}
pub(crate) fn ensure_orders_are_tradable(orders: &[Order]) -> crate::TastyResult<()> {
for order in orders {
ensure_legs_are_tradable(order.legs())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::order::{
Action, OrderBuilder, OrderLegBuilder, OrderType, PriceEffect, TimeInForce,
};
use rust_decimal::Decimal;
fn leg(instrument_type: InstrumentType, symbol: &str) -> OrderLeg {
OrderLegBuilder::default()
.instrument_type(instrument_type)
.symbol(symbol)
.quantity(Decimal::ONE)
.action(Action::BuyToOpen)
.build()
.expect("a valid leg")
}
fn order(legs: Vec<OrderLeg>) -> Order {
OrderBuilder::default()
.time_in_force(TimeInForce::Day)
.order_type(OrderType::Limit)
.price(Decimal::ONE)
.price_effect(PriceEffect::Debit)
.legs(legs)
.build()
.expect("a valid order")
}
#[test]
fn a_cryptocurrency_leg_is_refused_while_the_suspension_stands() {
let error = ensure_legs_are_tradable(&[leg(InstrumentType::Cryptocurrency, "BTC/USD")])
.expect_err("crypto order routing is suspended");
assert!(matches!(error, crate::TastyTradeError::Precondition(_)));
assert!(!error.is_retryable(), "nothing was sent");
let rendered = format!("{error}");
assert!(rendered.contains("2026-06-29"), "{rendered}");
assert!(rendered.contains("developer.tastytrade.com"), "{rendered}");
assert!(
rendered.contains("market data are unaffected"),
"the message must not imply crypto data is gone too: {rendered}"
);
}
#[test]
fn every_other_instrument_type_is_untouched() {
for instrument_type in [
InstrumentType::Equity,
InstrumentType::EquityOption,
InstrumentType::Future,
InstrumentType::FutureOption,
InstrumentType::Bond,
InstrumentType::Warrant,
] {
assert!(
ensure_legs_are_tradable(&[leg(instrument_type.clone(), "AAPL")]).is_ok(),
"{instrument_type:?} must still be tradable"
);
}
}
#[test]
fn one_cryptocurrency_component_refuses_the_whole_container() {
let mixed = vec![
order(vec![leg(InstrumentType::Equity, "AAPL")]),
order(vec![leg(InstrumentType::Cryptocurrency, "BTC/USD")]),
];
assert!(ensure_orders_are_tradable(&mixed).is_err());
assert!(
ensure_orders_are_tradable(&mixed[..1]).is_ok(),
"the equity component on its own is fine"
);
}
#[test]
fn a_mixed_leg_order_is_refused() {
assert!(
ensure_legs_are_tradable(&[
leg(InstrumentType::Equity, "AAPL"),
leg(InstrumentType::Cryptocurrency, "BTC/USD"),
])
.is_err()
);
}
#[test]
fn the_suspension_and_the_documentation_agree() {
let refused =
ensure_legs_are_tradable(&[leg(InstrumentType::Cryptocurrency, "BTC/USD")]).is_err();
assert_eq!(
refused, !CRYPTOCURRENCY_TRADING_ENABLED,
"the guard and the switch disagree; if cryptocurrency API trading has \
been re-enabled, update src/lib.rs, Doc/API_Coverage_Status.md, \
Doc/Instruments_Implementation_Status.md and the crate docs to match"
);
}
}