Skip to content

Commit 6be5a50

Browse files
committed
Fix BitMEX instrument bootstrap for unknown types
- Deserialize instruments per-item; skip unknown rows with a warning - Error if all rows fail to deserialize (venue schema change guard) - Add `BitmexInstrumentState::Unknown` to tolerate new venue states - Route legacy futures, spreads, and reference basket to parsers - Rename `StockPerpetual` to `TradFiPerpetual`
1 parent 5e23d3b commit 6be5a50

4 files changed

Lines changed: 81 additions & 20 deletions

File tree

RELEASES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Released on TBD (UTC).
77
- Added Binance Futures funding-rate history support in Rust
88
- Added Binance Futures ticker data support in Rust
99
- Added Binance order-list submission in Rust
10+
- Added `BitmexInstrumentState::Unknown` to tolerate unrecognized venue states without bootstrap failure
11+
- Added BitMEX legacy futures, spreads, and reference basket instrument parsing
1012
- Added cache order index crash-recovery restore for Redis and Postgres adapters (Rust)
1113
- Added Hyperliquid builder attribution opt-out
1214
- Added Hyperliquid historical trade requests
@@ -36,6 +38,7 @@ Released on TBD (UTC).
3638
- Changed Redis cache account/order/position storage to event logs; clear old typed state (Rust)
3739
- Changed `SyntheticInstrument` fallible methods to return `SyntheticInstrumentError` instead of `anyhow::Error` (Rust)
3840
- Changed WebSocket and socket `reconnect_timeout_ms` to bound only connection establishment (Rust)
41+
- Renamed `BitmexInstrumentType::StockPerpetual` to `TradFiPerpetual` (covers equities, FX, and commodities)
3942
- Renamed Rust/PyO3 instrument `tick_scheme_name` to `tick_scheme`; Cython keeps `tick_scheme_name`
4043

4144
### Security
@@ -57,6 +60,7 @@ Released on TBD (UTC).
5760
- Fixed Binance Futures node panic on `BNFCR` Credits Trading Mode balances
5861
- Fixed Binance Spot expired order handling
5962
- Fixed Binance Spot/Futures WebSocket connection pool race (#4244), thanks @filipmacek
63+
- Fixed BitMEX instrument bootstrap aborting on any row deserialize failure (#4283), thanks for reporting @seungpyoson
6064
- Fixed blocking Python HTTP functions holding the GIL for the full request duration
6165
- Fixed Blockchain snapshot bootstrap checks
6266
- Fixed Blockchain pool-event replay to require durable timestamps before checkpoints
@@ -78,6 +82,7 @@ Released on TBD (UTC).
7882
- Fixed `HttpClient` rejecting invalid response header keys instead of silently dropping them (Rust)
7983
- Fixed Hyperliquid bracket trigger-child statuses and atomic market fills orphaning orders at submission (#4160), thanks @sonnymai
8084
- Fixed Hyperliquid cancel-replace fill stranding on a dropped `ACCEPTED` (#4270), thanks for reporting @AlphaTraderK
85+
- Fixed `Instrument` rejecting negative `min_price`, preventing spread instruments from loading in Python
8186
- Fixed Interactive Brokers reconnect startup handling (#4210), thanks @faysou
8287
- Fixed Interactive Brokers to use `permId` for stable order identity (#4276), thanks @faysou
8388
- Fixed Kraken spot WebSocket dead-connection detection with an idle timeout (#4275), thanks @folknor

crates/adapters/bitmex/src/common/enums.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -640,10 +640,10 @@ pub enum BitmexInstrumentType {
640640
#[serde(rename = "FFICSX")]
641641
PredictionMarket,
642642

643-
/// Stock-based Perpetual Contracts (e.g., SPY, equity derivatives).
644-
/// CFI code FFSCSX - financial future on stocks, cash settled.
643+
/// TradFi Perpetual Contracts (equities, FX, and commodities).
644+
/// CFI code FFSCSX - financial future on non-crypto underlyings, cash settled.
645645
#[serde(rename = "FFSCSX")]
646-
StockPerpetual,
646+
TradFiPerpetual,
647647

648648
/// Perpetual Contracts (crypto).
649649
#[serde(rename = "FFWCSX")]
@@ -828,6 +828,9 @@ pub enum BitmexInstrumentState {
828828
Settled,
829829
/// Instrument is delisted.
830830
Delisted,
831+
/// Unrecognized instrument state received from the venue.
832+
#[serde(other)]
833+
Unknown,
831834
}
832835

833836
impl From<&BitmexInstrumentState> for MarketStatusAction {
@@ -838,6 +841,7 @@ impl From<&BitmexInstrumentState> for MarketStatusAction {
838841
BitmexInstrumentState::Settled => Self::Close,
839842
BitmexInstrumentState::Unlisted => Self::NotAvailableForTrading,
840843
BitmexInstrumentState::Delisted => Self::NotAvailableForTrading,
844+
BitmexInstrumentState::Unknown => Self::NotAvailableForTrading,
841845
}
842846
}
843847
}
@@ -991,7 +995,7 @@ mod tests {
991995
r#""FFWCSF""#
992996
);
993997
assert_eq!(
994-
serde_json::to_string(&BitmexInstrumentType::StockPerpetual).unwrap(),
998+
serde_json::to_string(&BitmexInstrumentType::TradFiPerpetual).unwrap(),
995999
r#""FFSCSX""#
9961000
);
9971001
assert_eq!(
@@ -1085,7 +1089,7 @@ mod tests {
10851089
);
10861090
assert_eq!(
10871091
serde_json::from_str::<BitmexInstrumentType>(r#""FFSCSX""#).unwrap(),
1088-
BitmexInstrumentType::StockPerpetual
1092+
BitmexInstrumentType::TradFiPerpetual
10891093
);
10901094
assert_eq!(
10911095
serde_json::from_str::<BitmexInstrumentType>(r#""IFXXXP""#).unwrap(),
@@ -1323,10 +1327,20 @@ mod tests {
13231327
BitmexInstrumentState::Delisted,
13241328
MarketStatusAction::NotAvailableForTrading
13251329
)]
1330+
#[case(
1331+
BitmexInstrumentState::Unknown,
1332+
MarketStatusAction::NotAvailableForTrading
1333+
)]
13261334
fn test_bitmex_instrument_state_to_market_status_action(
13271335
#[case] state: BitmexInstrumentState,
13281336
#[case] expected: MarketStatusAction,
13291337
) {
13301338
assert_eq!(MarketStatusAction::from(&state), expected);
13311339
}
1340+
1341+
#[rstest]
1342+
fn test_bitmex_instrument_state_unknown_deserializes_from_unrecognized_string() {
1343+
let state: BitmexInstrumentState = serde_json::from_str(r#""SomeFutureState""#).unwrap();
1344+
assert_eq!(state, BitmexInstrumentState::Unknown);
1345+
}
13321346
}

crates/adapters/bitmex/src/http/client.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,9 +512,13 @@ impl BitmexRawHttpClient {
512512

513513
/// Get all instruments.
514514
///
515+
/// Instruments that cannot be deserialized (e.g. unknown fields for new BitMEX
516+
/// instrument types) are skipped with a warning rather than failing the whole
517+
/// response.
518+
///
515519
/// # Errors
516520
///
517-
/// Returns an error if the request fails, the response cannot be parsed, or the API returns an error.
521+
/// Returns an error if the HTTP request fails or the response is not a JSON array.
518522
pub async fn get_instruments(
519523
&self,
520524
active_only: bool,
@@ -524,8 +528,29 @@ impl BitmexRawHttpClient {
524528
} else {
525529
"/instrument"
526530
};
527-
self.send_request::<_, ()>(Method::GET, path, None, None, false)
528-
.await
531+
let raw: Vec<serde_json::Value> = self
532+
.send_request::<_, ()>(Method::GET, path, None, None, false)
533+
.await?;
534+
535+
let raw_len = raw.len();
536+
let mut instruments = Vec::with_capacity(raw_len);
537+
538+
for value in raw {
539+
match serde_json::from_value::<BitmexInstrument>(value) {
540+
Ok(inst) => instruments.push(inst),
541+
Err(e) => {
542+
log::warn!("Skipping instrument that could not be deserialized: {e}");
543+
}
544+
}
545+
}
546+
547+
if raw_len > 0 && instruments.is_empty() {
548+
return Err(BitmexHttpError::JsonError(format!(
549+
"All {raw_len} instrument(s) failed to deserialize; venue schema may have changed"
550+
)));
551+
}
552+
553+
Ok(instruments)
529554
}
530555

531556
/// Requests the current server time from BitMEX.

crates/adapters/bitmex/src/http/parse.rs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ pub fn parse_instrument_any(
101101
BitmexInstrumentState::Open | BitmexInstrumentState::Closed => {}
102102
state @ (BitmexInstrumentState::Unlisted
103103
| BitmexInstrumentState::Settled
104-
| BitmexInstrumentState::Delisted) => {
104+
| BitmexInstrumentState::Delisted
105+
| BitmexInstrumentState::Unknown) => {
105106
return InstrumentParseResult::Inactive { symbol, state };
106107
}
107108
}
@@ -134,7 +135,7 @@ pub fn parse_instrument_any(
134135
error: e.to_string(),
135136
},
136137
},
137-
BitmexInstrumentType::FuturesSpread => {
138+
BitmexInstrumentType::FuturesSpread | BitmexInstrumentType::FuturesSpreads => {
138139
match parse_crypto_futures_spread_instrument(instrument, ts_init) {
139140
Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
140141
Err(e) => InstrumentParseResult::Failed {
@@ -144,8 +145,10 @@ pub fn parse_instrument_any(
144145
},
145146
}
146147
}
147-
BitmexInstrumentType::PredictionMarket => {
148-
// Prediction markets work similarly to futures (bounded 0-100, cash settled)
148+
BitmexInstrumentType::PredictionMarket
149+
| BitmexInstrumentType::LegacyFutures
150+
| BitmexInstrumentType::LegacyFuturesN => {
151+
// Prediction markets and legacy futures share the futures field structure
149152
match parse_futures_instrument(instrument, ts_init) {
150153
Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
151154
Err(e) => InstrumentParseResult::Failed {
@@ -159,11 +162,12 @@ pub fn parse_instrument_any(
159162
| BitmexInstrumentType::CryptoIndex
160163
| BitmexInstrumentType::FxIndex
161164
| BitmexInstrumentType::LendingIndex
165+
| BitmexInstrumentType::ReferenceBasket
162166
| BitmexInstrumentType::VolatilityIndex
163167
| BitmexInstrumentType::StockIndex
164168
| BitmexInstrumentType::YieldIndex => {
165-
// Parse index instruments as perpetuals for cache purposes
166-
// They need to be in cache for WebSocket price updates
169+
// Parse index and reference basket instruments for cache purposes;
170+
// they are needed for WebSocket price updates
167171
match parse_index_instrument(instrument, ts_init) {
168172
Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
169173
Err(e) => InstrumentParseResult::Failed {
@@ -174,15 +178,14 @@ pub fn parse_instrument_any(
174178
}
175179
}
176180

177-
// Explicitly list unsupported types for clarity
178-
BitmexInstrumentType::StockPerpetual
181+
// TradFi perpetuals (FFSCSX) parse correctly but CryptoPerpetual carries
182+
// AssetClass::Cryptocurrency, which misclassifies equity/FX/commodity perps.
183+
// Keep unsupported until a PerpetualContract parse path is wired up.
184+
// Options require a strike price field not yet present in BitmexInstrument.
185+
BitmexInstrumentType::TradFiPerpetual
179186
| BitmexInstrumentType::CallOption
180187
| BitmexInstrumentType::PutOption
181188
| BitmexInstrumentType::SwapRate
182-
| BitmexInstrumentType::ReferenceBasket
183-
| BitmexInstrumentType::LegacyFutures
184-
| BitmexInstrumentType::LegacyFuturesN
185-
| BitmexInstrumentType::FuturesSpreads
186189
| BitmexInstrumentType::Other => InstrumentParseResult::Unsupported {
187190
symbol,
188191
instrument_type,
@@ -1199,6 +1202,20 @@ mod tests {
11991202
);
12001203
}
12011204

1205+
#[rstest]
1206+
fn test_parse_instrument_any_skips_unknown_instrument_state() {
1207+
let json_data = load_test_json("http_get_instrument_xbtusd.json");
1208+
let mut instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1209+
instrument.state = BitmexInstrumentState::Unknown;
1210+
1211+
let result = parse_instrument_any(&instrument, UnixNanos::default());
1212+
1213+
assert!(
1214+
matches!(result, InstrumentParseResult::Inactive { .. }),
1215+
"expected Inactive for unknown state, was {result:?}"
1216+
);
1217+
}
1218+
12021219
#[rstest]
12031220
fn test_parse_instrument_any_parses_active_crypto_futures_spread() {
12041221
let json_data = load_test_json("http_get_instrument_xbtm26_xbtu26_spread.json");

0 commit comments

Comments
 (0)