//! Regression fixtures for the approved Binance execution-checklist units.
use std::collections::HashMap;
use rust_decimal::Decimal;
use serde_json::Value;
use super::{BinanceAdapter, BinanceAggregateTradesRequest, BinanceMarket, private, rest, stream};
use crate::error::{Error, Result};
use crate::request::OrderRequest;
use crate::types::{
Exchange, Feed, Interval, Market, MarketEvent, Side, Size, Subscription, Timestamp,
};
fn spot() -> BinanceAdapter {
BinanceAdapter::spot().with_credentials("key", "secret")
}
fn usd_m() -> BinanceAdapter {
BinanceAdapter::usd_m_futures().with_credentials("key", "secret")
}
fn spot_market() -> Market {
Market::spot(Exchange::Binance, "BTC", "USDT")
}
fn usd_m_market() -> Market {
Market::perpetual(Exchange::Binance, "BTC", "USDT")
}
fn signed_params(request: &crate::transport::HttpRequest) -> String {
let target = request.target();
let (params, _) = target
.split_once("&signature=")
.expect("a signed Binance request");
let (params, _) = params
.rsplit_once("×tamp=")
.expect("a timestamped Binance request");
params.to_string()
}
fn decode(adapter: &BinanceAdapter, market: Market, frame: &str) -> Result<MarketEvent> {
let symbol = adapter.symbol(&market)?.to_ascii_lowercase();
stream::decode(&HashMap::from([(symbol, market)]), frame)?
.ok_or_else(|| Error::decode("fixture produced no market event"))
}
#[test]
fn spot_aggregate_trades_uses_its_rest_contract_and_fixture() {
let request = BinanceAggregateTradesRequest::new(spot_market())
.with_from_id(26_129)
.limit(50);
assert_eq!(
rest::aggregate_trades_request(&spot(), &request)
.expect("a Spot aggregate-trade request")
.target(),
"/api/v3/aggTrades?symbol=BTCUSDT&fromId=26129&limit=50"
);
let values: Vec<Value> = super::parse::json(
r#"[{"a":26129,"p":"0.01633102","q":"4.70443515","f":27781,"l":27784,"T":1498793709153,"m":true,"M":true}]"#,
"Spot aggregate trades",
)
.expect("the official Spot fixture");
let raw = serde_json::from_value::<super::parse::RawAggregateTrade>(values[0].clone())
.expect("the Spot aggregate fields");
let trade =
super::parse::aggregate_trade(&spot_market(), &raw, &values[0]).expect("an aggregate");
assert_eq!(
(
trade.aggregate_id,
trade.first_trade_id,
trade.last_trade_id
),
(26_129, 27_781, 27_784)
);
assert_eq!(trade.normal_quantity, None);
assert_eq!(trade.best_price_match, Some(true));
assert_eq!(trade.taker_side, Side::Sell);
assert!(trade.raw_json.contains("\"M\":true"));
}
#[test]
fn usd_m_aggregate_trades_uses_its_rest_contract_and_fixture() {
let request = BinanceAggregateTradesRequest::new(usd_m_market())
.start_time(Timestamp::from_millis(1_623_319_461_670))
.end_time(Timestamp::from_millis(1_623_319_462_670));
assert_eq!(
rest::aggregate_trades_request(&usd_m(), &request)
.expect("a USD-M aggregate-trade request")
.target(),
"/fapi/v1/aggTrades?symbol=BTCUSDT&startTime=1623319461670&endTime=1623319462670&limit=500"
);
let values: Vec<Value> = super::parse::json(
r#"[{"a":26130,"p":"0.01633103","q":"1.2","nq":"1.00000000","f":27785,"l":27785,"T":1498793709253,"m":false}]"#,
"USD-M aggregate trades",
)
.expect("the official USD-M fixture");
let raw = serde_json::from_value::<super::parse::RawAggregateTrade>(values[0].clone())
.expect("the USD-M aggregate fields");
let trade =
super::parse::aggregate_trade(&usd_m_market(), &raw, &values[0]).expect("an aggregate");
assert_eq!(
trade.normal_quantity.expect("normal quantity").to_string(),
"1.00000000"
);
assert_eq!(trade.best_price_match, None);
assert_eq!(trade.taker_side, Side::Buy);
}
#[test]
fn spot_new_order_and_cancel_order_use_the_spot_contract_and_fixtures() {
let market = spot_market();
let order = OrderRequest::limit(
market.clone(),
Side::Buy,
Size::Base(Decimal::new(1, 2)),
Decimal::from(100_000),
);
assert_eq!(
signed_params(&private::place_order_request(&spot(), &order).expect("a Spot order")),
"/api/v3/order?symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=0.01&price=100000&timeInForce=GTC&newOrderRespType=RESULT"
);
assert_eq!(
signed_params(
&private::cancel_order_request(&spot(), &market, "28").expect("a Spot cancellation")
),
"/api/v3/order?symbol=BTCUSDT&orderId=28"
);
for (fixture, expected_status) in [
(
r#"{"symbol":"BTCUSDT","orderId":28,"side":"BUY","status":"NEW","price":"100000","origQty":"0.01","executedQty":"0","transactTime":1499827319559}"#,
crate::types::OrderStatus::Open,
),
(
r#"{"symbol":"BTCUSDT","orderId":28,"side":"BUY","status":"CANCELED","price":"100000","origQty":"0.01","executedQty":"0","transactTime":1499827319559}"#,
crate::types::OrderStatus::Cancelled,
),
] {
let raw: super::parse::RawOrder =
super::parse::json(fixture, "Spot order").expect("the Spot order fixture");
assert_eq!(
super::parse::order(&market, &raw)
.expect("a Spot order response")
.status,
expected_status
);
}
}
#[test]
fn usd_m_new_order_and_cancel_order_use_the_futures_contract_and_fixtures() {
let market = usd_m_market();
let order = OrderRequest::limit(
market.clone(),
Side::Sell,
Size::Base(Decimal::new(1, 2)),
Decimal::from(100_000),
);
assert_eq!(
signed_params(&private::place_order_request(&usd_m(), &order).expect("a USD-M order")),
"/fapi/v1/order?symbol=BTCUSDT&side=SELL&type=LIMIT&quantity=0.01&price=100000&timeInForce=GTC&newOrderRespType=RESULT"
);
assert_eq!(
signed_params(
&private::cancel_order_request(&usd_m(), &market, "28").expect("a USD-M cancellation")
),
"/fapi/v1/order?symbol=BTCUSDT&orderId=28"
);
for (fixture, expected_status) in [
(
r#"{"symbol":"BTCUSDT","orderId":28,"side":"SELL","status":"NEW","price":"100000","origQty":"0.01","executedQty":"0","updateTime":1499827319559}"#,
crate::types::OrderStatus::Open,
),
(
r#"{"symbol":"BTCUSDT","orderId":28,"side":"SELL","status":"CANCELED","price":"100000","origQty":"0.01","executedQty":"0","updateTime":1499827319559}"#,
crate::types::OrderStatus::Cancelled,
),
] {
let raw: super::parse::RawOrder =
super::parse::json(fixture, "USD-M order").expect("the USD-M order fixture");
assert_eq!(
super::parse::order(&market, &raw)
.expect("a USD-M order response")
.status,
expected_status
);
}
}
#[test]
fn order_detail_responses_keep_venue_specific_fields_and_the_payload() {
let spot = private::order_response_from_body(
&spot_market(),
r#"{"symbol":"BTCUSDT","orderId":28,"orderListId":-1,"clientOrderId":"spot-client","transactTime":1499827319559,"price":"100","origQty":"0.01","executedQty":"0.005","cummulativeQuoteQty":"0.5","status":"PARTIALLY_FILLED","timeInForce":"GTC","type":"LIMIT","side":"BUY","selfTradePreventionMode":"EXPIRE_MAKER","futureField":"kept"}"#,
)
.expect("a Spot provider response");
assert_eq!(spot.client_order_id.as_deref(), Some("spot-client"));
assert_eq!(spot.order_list_id.as_deref(), Some("-1"));
assert_eq!(
spot.cumulative_quote_quantity.expect("quote").to_string(),
"0.5"
);
assert_eq!(
spot.self_trade_prevention_mode.as_deref(),
Some("EXPIRE_MAKER")
);
assert!(spot.raw_json.contains("\"futureField\":\"kept\""));
let futures = private::order_response_from_body(
&usd_m_market(),
r#"{"symbol":"BTCUSDT","orderId":29,"clientOrderId":"futures-client","price":"100","avgPrice":"99.5","origQty":"0.01","executedQty":"0.005","cumQty":"0.005","cumQuote":"0.4975","status":"PARTIALLY_FILLED","timeInForce":"GTC","type":"LIMIT","reduceOnly":false,"closePosition":false,"side":"SELL","positionSide":"SHORT","stopPrice":"0","workingType":"CONTRACT_PRICE","priceProtect":true,"origType":"LIMIT","priceMatch":"NONE","selfTradePreventionMode":"EXPIRE_TAKER","goodTillDate":1700000000000,"updateTime":1499827319559,"futureField":"kept"}"#,
)
.expect("a USD-M provider response");
assert_eq!(futures.client_order_id.as_deref(), Some("futures-client"));
assert_eq!(futures.average_price.expect("average").to_string(), "99.5");
assert_eq!(futures.position_side.as_deref(), Some("SHORT"));
assert_eq!(futures.price_protect, Some(true));
assert_eq!(
futures.good_till_date.expect("good till").as_millis(),
1_700_000_000_000
);
assert!(futures.raw_json.contains("\"futureField\":\"kept\""));
}
#[test]
fn spot_stream_units_use_the_documented_names_and_fixtures() {
let subscription = Subscription::new()
.market(spot_market())
.feed(Feed::Trades)
.feed(Feed::OrderBook)
.feed(Feed::Ticker)
.feed(Feed::Candles(Interval::Min1));
let frames = stream::subscribe_frames(&spot(), &subscription).expect("one Spot subscription");
let [(url, frame)] = frames.as_slice() else {
panic!("Spot feeds must share one socket")
};
assert_eq!(*url, super::SPOT_WEBSOCKET_URL);
let frame: Value = serde_json::from_str(frame).expect("a subscription frame");
assert_eq!(
frame["params"],
serde_json::json!([
"btcusdt@trade",
"btcusdt@depth20@100ms",
"btcusdt@ticker",
"btcusdt@kline_1m"
])
);
let fixtures = [
(
r#"{"stream":"btcusdt@trade","data":{"e":"trade","E":1672515782136,"s":"BTCUSDT","t":12345,"p":"0.001","q":"100","T":1672515782136,"m":true,"M":true}}"#,
"trade",
),
(
r#"{"stream":"btcusdt@depth20@100ms","data":{"lastUpdateId":160,"bids":[["0.0024","10"]],"asks":[["0.0026","100"]]}}"#,
"depth",
),
(
r#"{"stream":"btcusdt@ticker","data":{"e":"24hrTicker","E":1672515782136,"s":"BTCUSDT","p":"0.0015","P":"250.00","c":"0.0025","h":"0.0025","l":"0.0010","v":"10000","q":"18","C":86400000}}"#,
"ticker",
),
(
r#"{"stream":"btcusdt@kline_1m","data":{"e":"kline","E":1672515782136,"s":"BTCUSDT","k":{"t":1672515780000,"i":"1m","o":"0.0010","c":"0.0020","h":"0.0025","l":"0.0015","v":"1000","q":"1.0000","x":false}}}"#,
"kline",
),
];
for (fixture, kind) in fixtures {
let event = decode(&spot(), spot_market(), fixture).expect("a Spot stream fixture");
assert!(
matches!(
(kind, event),
("trade", MarketEvent::Trade(_))
| ("depth", MarketEvent::OrderBook(_))
| ("ticker", MarketEvent::Ticker(_))
| ("kline", MarketEvent::Candle(_))
),
"wrong event for {kind}"
);
}
}
#[test]
fn spot_user_data_stream_signature_uses_the_signed_websocket_method() {
let frame = private::spot_user_data_subscribe_frame(&spot()).expect("a signed frame");
let frame: Value = serde_json::from_str(&frame).expect("a JSON frame");
assert_eq!(frame["method"], "userDataStream.subscribe.signature");
assert_eq!(frame["params"]["apiKey"], "key");
assert_eq!(frame["params"]["recvWindow"], 60_000);
assert!(frame["params"]["timestamp"].as_i64().is_some());
let signature = frame["params"]["signature"]
.as_str()
.expect("an HMAC signature");
assert_eq!(signature.len(), 64);
assert!(signature.bytes().all(|byte| byte.is_ascii_hexdigit()));
}
#[test]
fn usd_m_stream_units_use_the_split_entry_points_and_fixtures() {
let subscription = Subscription::new()
.market(usd_m_market())
.feed(Feed::OrderBook)
.feed(Feed::Ticker)
.feed(Feed::Candles(Interval::Min1));
let frames = stream::subscribe_frames(&usd_m(), &subscription).expect("USD-M subscriptions");
assert_eq!(frames.len(), 2);
let public: Value = serde_json::from_str(&frames[0].1).expect("a public frame");
let market: Value = serde_json::from_str(&frames[1].1).expect("a market frame");
assert_eq!(frames[0].0, super::USD_M_PUBLIC_WEBSOCKET_URL);
assert_eq!(
public["params"],
serde_json::json!(["btcusdt@depth20@100ms"])
);
assert_eq!(frames[1].0, super::USD_M_MARKET_WEBSOCKET_URL);
assert_eq!(
market["params"],
serde_json::json!(["btcusdt@ticker", "btcusdt@kline_1m"])
);
let fixtures = [
(
r#"{"stream":"btcusdt@depth20@100ms","data":{"e":"depthUpdate","E":1571889248277,"T":1571889248276,"s":"BTCUSDT","U":390497796,"u":390497878,"pu":390497794,"b":[["7403.89","0.002"]],"a":[["7405.96","3.4"]]}}"#,
"depth",
),
(
r#"{"stream":"btcusdt@ticker","data":{"e":"24hrTicker","E":123456789,"s":"BTCUSDT","p":"0.0015","P":"250.00","c":"0.0025","h":"0.0025","l":"0.0010","v":"10000","q":"18","C":86400000}}"#,
"ticker",
),
(
r#"{"stream":"btcusdt@kline_1m","data":{"e":"kline","E":1499404907056,"s":"BTCUSDT","k":{"t":1499404860000,"i":"1m","o":"0.0010","c":"0.0020","h":"0.0025","l":"0.0015","v":"1000","q":"1.0000","x":true}}}"#,
"kline",
),
];
for (fixture, kind) in fixtures {
let event = decode(&usd_m(), usd_m_market(), fixture).expect("a USD-M stream fixture");
assert!(
matches!(
(kind, event),
("depth", MarketEvent::OrderBook(_))
| ("ticker", MarketEvent::Ticker(_))
| ("kline", MarketEvent::Candle(_))
),
"wrong event for {kind}"
);
}
}
#[test]
fn usd_m_aggregate_trade_stream_stays_blocked_at_the_common_trade_boundary() {
let subscription = Subscription::new()
.market(usd_m_market())
.feed(Feed::Trades);
assert!(matches!(
stream::subscribe_frames(&usd_m(), &subscription),
Err(Error::Unsupported { .. })
));
assert_eq!(
BinanceMarket::UsdMFutures.market_kind(),
crate::types::MarketKind::Perpetual
);
}